Herança é a primeira ferramenta que a maioria das pessoas usa e a que envelhece pior. O motivo é simples: uma subclasse herda tudo, inclusive as partes que não se aplicam a ela.
O problema do pinguim
# inheritance for reuse: the shape that goes wrong
class Animal:
def __init__(self, name): self.name = name
def speak(self): return '...'
def fly(self): return f"{self.name} flies away"
class Bird(Animal):
def speak(self): return f"{self.name} tweets"
class Penguin(Bird):
pass
p = Penguin('Pingu')
print(p.speak())
print(p.fly()) # inherited, and wrong
Ele imprime:
Pingu tweets
Pingu flies away
Um pinguim é inquestionavelmente uma ave, e a hierarquia está inquestionavelmente errada. Não houve erro de modelagem — fly foi colocado na classe base porque a maioria das aves voa, o que era verdade quando aquilo foi escrito.
Os remendos habituais pioram a situação. Sobrescreva fly para levantar erro e você tem uma subclasse que falha na interface do pai. Mova fly para uma classe FlyingBird e você vai ter que fazer tudo de novo na primeira vez que encontrar outra coisa que não voa.
Composição: a capacidade é um objeto
# composition: the capability is an object, not an ancestor
class Wings:
def fly(self, name): return f"{name} flies away"
class Bird2:
def __init__(self, name, wings=None):
self.name, self.wings = name, wings
def speak(self): return f"{self.name} tweets"
def fly(self):
if self.wings is None:
raise TypeError(f"{self.name} cannot fly")
return self.wings.fly(self.name)
print(Bird2('Robin', Wings()).fly())
try:
Bird2('Pingu').fly()
except TypeError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
Robin flies away
TypeError: Pingu cannot fly
Voar agora é algo que uma ave tem, não algo que ela é. Acrescentar uma ave que não voa não exige classe nova nem mudança de hierarquia.
O ganho é poder trocar comportamento
O argumento de verdade não é taxonomia. É que uma capacidade composta pode ser trocada em tempo de execução e substituída num teste.
# swapping behaviour at runtime is the payoff
class JsonFormat:
def render(self, row): return f'{{"name": "{row}"}}'
class CsvFormat:
def render(self, row): return f'{row}'
class Report:
def __init__(self, fmt): self.fmt = fmt
def emit(self, rows): return [self.fmt.render(r) for r in rows]
print(Report(JsonFormat()).emit(['ada']))
print(Report(CsvFormat()).emit(['ada']))
Ele imprime:
['{"name": "ada"}']
['ada']
Um Report só. Como herança isso seria JsonReport e CsvReport, e um terceiro formato significaria uma terceira classe — além de um problema de verdade no dia em que você quiser um relatório que é os dois.
Testar mostra a diferença com mais clareza. Passe um formatador falso e você testou o Report sem tocar em JSON. Com herança você teria que criar uma subclasse para interceptar.
Quando herança está certa
Ela tem uma função, e é mais estreita do que as pessoas usam: quando a relação é genuinamente “é um” e a classe base é abstrata — ela define uma interface em vez de entregar comportamento que as subclasses talvez não queiram.
# inheritance is right when it is genuinely 'is a' and the base is abstract
class Shape:
def area(self): raise NotImplementedError
def describe(self): return f"{type(self).__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s ** 2
for s in (Circle(1), Square(2)):
print(s.describe())
Ele imprime:
Circle with area 3.14
Square with area 4.00
Shape promete area e oferece describe construído em cima dela. Nenhuma subclasse herda algo que não deveria ter, porque não há nada concreto para herdar. A parte 9 cobre ABCs e Protocols, que tornam essa promessa verificável.
O que lembrar
-
Herança dá à subclasse tudo, inclusive o que ela não deveria ter.
-
Use composição quando você está herdando para reaproveitar código, e não para declarar um tipo.
-
O benefício prático é trocar e falsificar comportamento, não pureza de modelagem.
-
Herança serve quando a base é abstrata e a relação é genuinamente “é um”.
A regra prática que sobrevive ao código real: se você está escrevendo uma subclasse para ter acesso a um método, você quer composição. Se está escrevendo uma para prometer uma interface, herança está bem.