Chapter 28, Intermediate
Inheritance and polymorphism
Inheritance reuses behavior and polymorphism allows treating different types the same way. I show how to use it, and especially when to prefer composition.
Code for this chapter: intermediario/cap28_heranca.py
Inherit and override
A child class receives everything from the parent class and can override whatever it wants. The result is polymorphism: the same code calls the same method name and each object responds in its own way:
class Animal:
def __init__(self, nome):
self.nome = nome
def falar(self):
return "..."
def apresentar(self):
return f"{self.nome} diz {self.falar()}"
class Cachorro(Animal):
def falar(self):
return "au"
class Gato(Animal):
def falar(self):
return "miau"
for animal in [Cachorro("Rex"), Gato("Mia"), Animal("Ser")]:
print(animal.apresentar())
Rex diz au
Mia diz miau
Ser diz ...
The apresentar is written once and works for all, because it calls self.falar(), and the self decides which version to run.
super(): reuse the parent
When the child needs to extend the parent's behavior, instead of replacing it, it calls the parent with super():
class Funcionario:
def __init__(self, nome, salario):
self.nome = nome
self.salario = salario
def bonus(self):
return self.salario * 0.1
class Gerente(Funcionario):
def __init__(self, nome, salario, equipe):
super().__init__(nome, salario)
self.equipe = equipe
def bonus(self):
return super().bonus() + 100 * len(self.equipe)
g = Gerente("Ana", 5000, ["Bia", "Caio"])
print(g.bonus())
700.0
Forgetting the super().__init__(...) is a classic mistake: the parent never initializes its attributes, and the error only appears later.
Hierarchies and the search order
Inheritance can have multiple levels. The order in which Python looks for a method is called MRO (method resolution order) and can be consulted:
class A:
pass
class B(A):
pass
class C(B):
pass
print(issubclass(C, A), isinstance(C(), B))
print([k.__name__ for k in C.__mro__])
True True
['C', 'B', 'A', 'object']
Multiple inheritance (class D(B, C)) exists and works, but it is a source of complexity. I address it in more detail at the advanced level.
Duck typing
Python does not require a hierarchy for polymorphism to work. If the object has the method you call, it works: "if it walks like a duck and quacks like a duck, it is a duck":
class Pato:
def falar(self):
return "quack"
class Robo:
def falar(self):
return "bip"
for coisa in (Pato(), Robo()):
print(coisa.falar())
quack
bip
Composition before inheritance
Inheritance means "is a". Composition means "has a". A Carro is a vehicle, but has an engine. I only inherit when the "is a" relationship is true and the child can be used anywhere the parent is expected. For everything else, I compose:
class Motor:
def ligar(self):
return "motor ligado"
class Carro:
def __init__(self):
self.motor = Motor()
def partir(self):
return self.motor.ligar()
print(Carro().partir())
motor ligado
Why I prefer composition
Inheritance couples the child to the internal details of the parent: changing the parent can break all children. Composition only couples to the public interface. When in doubt, start with composition. You can evolve to inheritance later, but undoing a hierarchy is costly.
Exercise 1
Geometric shapes
Create Forma with the method area() that raises NotImplementedError, and the subclasses Retangulo(largura, altura) and Circulo(raio). Write area_total(formas).