2.22. Herança¶
A herança permite que uma classe estenda outra classe – começar com todos os seus métodos e atributos e, então, adicionar ou sobrescrever o que for diferente. A original é chamada de base (ou pai); a extensão é chamada de subclasse.
Square reutiliza todos os métodos de Shape e sobrescreve aqueles que precisa especializar.¶
2.22.1. Definindo uma subclasse¶
A classe base vai entre os parênteses na linha class:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
Saída:
square with area 16
Square herda describe de Shape inalterado e sobrescreve area para retornar um valor real. O describe da base ainda funciona porque chama self.area() – que se resolve para a versão sobrescrita em tempo de execução.
2.22.2. super()¶
super() retorna um proxy que permite a um método chamar a versão da classe base de um método. O uso mais comum é chamar o __init__ da base a partir do __init__ de uma subclasse, para que a base possa inicializar seus próprios atributos:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
Esquecer super().__init__ é uma fonte comum de bugs: os atributos que a base teria definido nunca são definidos, e os métodos herdados que dependem deles falham mais tarde.
2.22.2.1. A classe base implícita¶
Toda classe herda implicitamente de object, a raiz da hierarquia de tipos do Python. Métodos como __repr__, __eq__ e toda a maquinaria por trás do acesso a atributos vêm de lá, mesmo quando uma classe não declara nenhuma base. Escrever class Foo(object): explicitamente e escrever class Foo: são equivalentes no Python moderno; a última é a forma convencional.
2.22.3. Quando a herança ajuda – e quando não ajuda¶
Use herança quando uma classe é genuinamente um tipo mais específico de outra, compartilhando a maior parte do comportamento. O teste clássico é a verificação “é-um”: um Square é um Shape.
Quando duas classes apenas por acaso compartilham alguns auxiliares, a herança é exagero. Recorra à composição: faça com que uma classe contenha uma instância da outra como atributo e a use por meio desse atributo. O formato é “tem-um”, não “é-um”:
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
Ambas funcionam. A versão por composição costuma ser melhor porque:
Mantém as interfaces pequenas.
WorkerComposesexpõe apenasrunelogger.WorkerInheritstambém expõelog– os chamadores podem escreverworker.log(...)diretamente, quer isso tenha sido pretendido ou não.Desacopla os tempos de vida. O logger pode ser trocado, compartilhado entre workers ou construído de forma diferente para cada worker, tudo sem tocar na classe
Worker. Uma subclasse fica presa à sua base.Evita colisões de nomes de método. Duas bases que ambas definem
runentram em conflito quando uma classe tenta herdar de ambas; dois atributos nunca colidem.
Uma regra prática:
Use herança quando a subclasse realmente é um tipo da base e todo método da base também faz sentido nela –
Squareé umShape;MemoryErroré umaException.Use composição para todo o resto – quando você só quer o comportamento de outra classe, não sua identidade. A maior parte do código real usa composição muito mais do que herança.