2.22. Herança

A herança permite que uma classe estenda outra classe – começa com todos os seus métodos e atributos, e depois adiciona ou substitui o que é diferente. A original chama-se base (ou pai); a extensão chama-se subclasse.

A base class Shape pointing down to a subclass Square; Square overrides one method (area) and inherits the rest.

Square reutiliza todos os métodos de Shape e substitui os que precisa de especializar.

2.22.1. Definir uma subclasse

A classe base vai entre 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())

Resultado:

square with area 16

Square herda describe de Shape sem alterações e substitui area para devolver um valor real. O describe da base ainda funciona porque chama self.area() – que resolve para a substituição em tempo de execução.

2.22.2. super()

super() devolve um proxy que permite que um método chame a versão da classe base de um método. O uso mais comum é chamar o __init__ da base a partir de um __init__ de subclasse para que a base possa inicializar os 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 erros: 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 a classe herda implicitamente de object, a raiz da hierarquia de tipos do Python. Métodos como __repr__, __eq__, e os mecanismos por trás do acesso a atributos vêm todos daí, 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, partilhando a maior parte do comportamento. O teste clássico é a verificação «é-um»: um Square é uma Shape.

Quando duas classes apenas partilham alguns auxiliares, a herança é excessiva. Recorra antes à composição: faça com que uma classe contenha uma instância da outra como atributo e use-a através desse atributo. A relação é «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 é geralmente melhor porque:

  • Mantém as interfaces pequenas. WorkerComposes expõe apenas run e logger. WorkerInherits também expõe log – os invocadores podem escrever worker.log(...) directamente, independentemente de isso ser intencional ou não.

  • Desacopla os ciclos de vida. O logger pode ser substituído, partilhado entre trabalhadores ou construído de forma diferente por trabalhador, tudo sem tocar na classe Worker. Uma subclasse está presa à sua base.

  • Evita colisões de nomes de métodos. Duas bases que definem ambas run colidem quando uma classe tenta herdar das duas; dois atributos nunca colidem.

Uma regra prática:

  • Use herança quando a subclasse realmente é um tipo da base e todos os métodos da base fazem sentido nela – Square é uma Shape; MemoryError é uma Exception.

  • Use composição para tudo o resto – quando apenas quer o comportamento de outra classe, não a sua identidade. A maior parte do código real usa composição muito mais do que usa herança.