Polymorphism

One of the great benefits of object-oriented programming is polymorphism: different types of objects sharing the same interface.

Polymorphism from inheritance

We've already seen many polymorphic objects, since inheritance almost always leads to polymorphism.

For example, we can call the eat method on both Panda object and Lion objects, passing in the same argument:

broc = Meal("Broccoli", "veggie")
pandey = Panda("Pandeybear", 6)
mufasa = Lion("Mufasa", 12)

pandey.eat(broc)
mufasa.eat(broc)

Now, the two objects may react differently to that method, due to differing implementations. The panda might say "Mmm yummy" and the lion might say "Gross, veggies!". But the point of polymorphism is that the interface was the same - the method name and argument types - which meant we could call that interface without knowing the exact details of the underlying implementation.

When we have a list of polymorphic objects, we can call the same method on each object even if we don't know exactly what type of object they are.

Consider this function that throws an animal party, making each animal in a list interact_with the other animals in the list:

def partytime(animals):
    for i in range(len(animals)):
        for j in range(i + 1, len(animals)):
            animals[i].interact_with(animals[j])

That list of animals can contain any Animal instance, since they all implement interact_with in some way.

jane_doe = Rabbit("Jane Doe", 2)
scar = Lion("Scar", 12)
elly = Elephant("Elly", 5)
pandy = Panda("PandeyBear", 4)
partytime([jane_doe, scar, elly, pandy])

That right there is the power of polymorphism.

Polymorphism from method interfaces

We've also seen a lot of polymorphism in the built-in Python object types, even though we haven't called it that.

For example, consider how many types we can pass to the len function:

len([1, 2, 3])
len("ahoy there!")
len({"CA": "Sacramento", "NY": "Albany"})
len(range(5))

The len() function is able to report the length of lists, strings, dicts, and ranges. All of those inherit from object, but len() doesn't work on any old object. So why does it work for those types?

It's actually because the list, str, dict, and range classes each define a method named __len__ that reports their length using various mechanisms. The global len() function works on any object that defines a __len__ method and returns a number. That means we can even make len() work on our own user-defined classes.

This type of polymorphism in a language is also known as duck typing: if it walks like a duck and quacks like a duck, then it must be a duck. 🦆The len() function doesn't care exactly what kind of object it's passed: if it has a __len__ method and that method takes 0 arguments, then that's good enough for Python.

Many languages are more strict than Python, and possibly for good reason, since you can get yourself in trouble with loosey-goosey duck typing. But you can also write highly flexible code. Tread carefully in these polymorphic waters!

➡️ Next up: Project 4: OOP quiz