Herança simples e sobrescrita de métodos

1. Fundamentos da Herança Simples

Herança é um dos pilares da programação orientada a objetos que permite que uma classe filha herde atributos e métodos de uma classe pai. Em Python, a sintaxe é direta: basta declarar a classe filha com o nome da classe pai entre parênteses.

class Animal:
    def __init__(self, nome):
        self.nome = nome

    def mover(self):
        return f"{self.nome} está se movendo"

class Cachorro(Animal):
    def latir(self):
        return f"{self.nome} está latindo"

# Uso
rex = Cachorro("Rex")
print(rex.mover())  # Herdou o método da classe pai
print(rex.latir())  # Método específico da filha

A classe Cachorro automaticamente tem acesso a todos os atributos e métodos de Animal. Isso promove reuso de código e estabelece uma relação hierárquica natural.

2. A Função super() e a Classe Base

A função super() é essencial para acessar métodos da classe pai dentro da classe filha. Ela é particularmente importante no construtor __init__, onde precisamos garantir que a inicialização da classe base ocorra corretamente.

class Animal:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

class Cachorro(Animal):
    def __init__(self, nome, idade, raca):
        super().__init__(nome, idade)  # Chama o __init__ da classe pai
        self.raca = raca

    def apresentar(self):
        return f"Sou {self.nome}, tenho {self.idade} anos e sou da raça {self.raca}"

# Uso
bob = Cachorro("Bob", 3, "Labrador")
print(bob.apresentar())

Boas práticas: Sempre chame super().__init__() como primeira linha do construtor da classe filha, a menos que tenha um motivo muito específico para não fazê-lo. Isso garante que todos os atributos da classe base sejam inicializados corretamente.

3. Sobrescrita de Métodos (Method Overriding)

Sobrescrita de métodos permite que a classe filha redefina um método herdado, fornecendo uma implementação específica para seu contexto. A assinatura do método (nome e parâmetros) deve ser mantida, mas o comportamento interno pode ser completamente diferente.

class Animal:
    def fazer_som(self):
        return "Som genérico de animal"

class Cachorro(Animal):
    def fazer_som(self):
        return "Au au!"

class Gato(Animal):
    def fazer_som(self):
        return "Miau!"

# Polimorfismo em ação
animais = [Cachorro(), Gato(), Animal()]
for animal in animais:
    print(animal.fazer_som())

Cada classe filha fornece sua própria implementação de fazer_som(), demonstrando como a sobrescrita permite comportamento polimórfico.

4. Controlando o Acesso com super() na Sobrescrita

A sobrescrita não precisa ser uma substituição total. Podemos usar super() para reutilizar a lógica do método pai e estendê-la com comportamento adicional.

class Veiculo:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.ligado = False

    def ligar(self):
        self.ligado = True
        return f"{self.marca} {self.modelo} ligado"

class Carro(Veiculo):
    def __init__(self, marca, modelo, portas):
        super().__init__(marca, modelo)  # Estende a inicialização
        self.portas = portas

    def ligar(self):
        resultado = super().ligar()  # Reutiliza a lógica do pai
        return f"{resultado} com {self.portas} portas"

# Uso
fusca = Carro("VW", "Fusca", 2)
print(fusca.ligar())

A diferença fundamental: substituir totalmente significa ignorar completamente o método pai, enquanto estender usa super() para aproveitar parte do comportamento original.

5. A Ordem de Resolução de Métodos (MRO) em Herança Simples

Em herança simples (uma única classe pai), a MRO (Method Resolution Order) é linear e previsível: a classe filha, depois a classe pai, e assim por diante até object.

class A:
    def metodo(self):
        return "Método de A"

class B(A):
    def metodo(self):
        return "Método de B"

class C(B):
    pass

# Visualizando a MRO
print(C.__mro__)
# Saída: (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

c = C()
print(c.metodo())  # Busca começa em C, vai para B, encontra o método

A MRO garante que o Python sempre encontre o método mais específico primeiro, seguindo a hierarquia de herança.

6. Herança e Atributos de Classe vs Instância

Atributos de classe são compartilhados entre todas as instâncias, incluindo as classes filhas. Isso pode levar a comportamentos inesperados se não for bem compreendido.

class Trabalhador:
    ferias = 30  # Atributo de classe compartilhado

class Estagiario(Trabalhador):
    ferias = 45  # Sobrescrita na classe filha

class Gerente(Trabalhador):
    pass  # Mantém o valor da classe pai

# Atributos de classe
print(Trabalhador.ferias)   # 30
print(Estagiario.ferias)    # 45
print(Gerente.ferias)       # 30

# Cuidado com mutabilidade!
class Empresa:
    funcionarios = []  # Lista compartilhada

class Filial(Empresa):
    pass

Empresa.funcionarios.append("João")
Filial.funcionarios.append("Maria")
print(Empresa.funcionarios)  # ['João', 'Maria'] - mesma lista!

Para evitar efeitos colaterais, prefira sempre inicializar atributos mutáveis no __init__ (atributos de instância), não como atributos de classe.

7. Boas Práticas e Armadilhas Comuns

Evite sobrescrita acidental: Tenha cuidado ao sobrescrever métodos especiais como __str__, __repr__ ou propriedades com @property.

class Produto:
    def __init__(self, preco):
        self._preco = preco

    @property
    def preco(self):
        return self._preco

class ProdutoComDesconto(Produto):
    @property
    def preco(self):  # Sobrescrita correta da property
        return self._preco * 0.9

Quando preferir composição: Se a relação entre classes é "tem um" em vez de "é um", considere composição.

# Em vez de herança
class CarroEletrico(Veiculo):  # "é um" veículo - OK
    pass

# Composição pode ser melhor
class Motor:
    def ligar(self):
        return "Motor ligado"

class Carro:
    def __init__(self):
        self.motor = Motor()  # "tem um" motor

Testando hierarquias: Use isinstance() e issubclass() para verificar relações.

class Animal: pass
class Cachorro(Animal): pass

rex = Cachorro()
print(isinstance(rex, Animal))     # True
print(isinstance(rex, Cachorro))   # True
print(issubclass(Cachorro, Animal)) # True

Herança simples é uma ferramenta poderosa quando usada corretamente. Lembre-se: modele hierarquias "é um" naturais, sempre chame super().__init__() em construtores, e prefira composição quando a relação não for claramente hierárquica.

Referências