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.

Uma classe base Shape apontando para baixo para uma subclasse Square; Square sobrescreve um método (area) e herda o restante.

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. WorkerComposes expõe apenas run e logger. WorkerInherits também expõe log – os chamadores podem escrever worker.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 run entram 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 é um Shape; MemoryError é uma Exception.

  • 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.