Polymorphism: one call, many classes
When several classes offer a method with the same name and meaning, code can call it without knowing which class it has. Each object runs its own version, and new kinds can be added without touching the caller.
- Give several classes a method with the same name, parameters and meaning
- Write code that calls that method without checking which class it has
- Add a new class that works with existing code without changing that code
Delivery costs work differently for standard, express and collect-in-store. One way to write the quote is a chain: if isinstance(option, StandardDelivery): … elif isinstance(option, ExpressDelivery): …. Every new delivery option means editing that chain, and every other place with a similar chain.
The alternative: each class has a method with the same name and meaning, cost(weight_kg). The caller just calls option.cost(weight), and Python runs the method of whichever class the object belongs to. This is polymorphism: one call, many forms.
The classes don't even need a common parent. Python only needs the object to have the method when it is called (this is called *duck typing*: if it has cost, it can be quoted).
Run each example to see what it prints. Running examples doesn't count toward progress.
class StandardDelivery: def cost(self, weight_kg): return 3.5 if weight_kg <= 2 else 3.5 + (weight_kg - 2) * 1.25 class ExpressDelivery: def cost(self, weight_kg): return 8.0 + weight_kg * 0.5 class CollectInStore: def cost(self, weight_kg): return 0.0 def quotes(options, weight_kg): for option in options: print(type(option).__name__, round(option.cost(weight_kg), 2)) quotes([StandardDelivery(), ExpressDelivery(), CollectInStore()], 4.0)
class EmailChannel: def __init__(self, address): self.address = address def send(self, message): return f"email to {self.address}: {message}" class SmsChannel: def __init__(self, number): self.number = number def send(self, message): return f"sms to {self.number}: {message[:20]}" class WebhookChannel: def __init__(self, url): self.url = url def send(self, message): return f"POST {self.url} {{\"text\": \"{message}\"}}" def notify(channels, message): receipts = [] for channel in channels: receipts.append(channel.send(message)) return receipts channels = [EmailChannel("nora_garcia@fastmail.com"), SmsChannel("+44 7700 900123"), WebhookChannel("https://hooks.example.com/orders")] for receipt in notify(channels, "Order O100282 has shipped"): print(receipt)
class EmailChannel: def send(self, message): return "email: " + message class PrinterChannel: def print_message(self, message): return "printed: " + message for channel in [EmailChannel(), PrinterChannel()]: print(channel.send("Order O100282 has shipped"))
When to use it: whenever you find yourself writing if isinstance(x, A) … elif isinstance(x, B) … to decide what to do. Move each branch into a method of its class.
Mistakes to avoid: methods with the same name but different parameters or return types (cost() returning a number in one class and text in another); one class printing where the others return; and keeping a type check in the caller "just in case", which defeats the point.
- Classes that offer the same method (same name, parameters and meaning) can be used interchangeably.
- The caller calls the method; each object runs its own version. No isinstance chain is needed.
- New kinds are added as new classes, without editing the code that uses them.
- Next: objects that hold other objects and hand work to them.