A herança é um dos conceitos fundamentais da programação orientada a objetos (POO). Em Python, ela permite que uma classe (chamada de subclasse ou classe filha) herde atributos e métodos de outra classe (chamada de superclasse ou classe mãe). Isso promove a reutilização de código, a hierarquia lógica e a extensibilidade. Ao dominar a herança, você conseguirá modelar sistemas mais complexos, evitando duplicação e facilitando a manutenção.

Nesta aula, vamos nos aprofundar na sintaxe da herança, no uso de super(), na sobrescrita de métodos e no mecanismo de resolução de ordem de métodos (MRO). Vamos também discutir boas práticas e apresentar exercícios para fixar o conteúdo.

Sintaxe

Em Python, para criar uma subclasse, basta definir uma nova classe e colocar o nome da superclasse entre parênteses na declaração. A sintaxe é simples e direta:

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

    def fazer_som(self):
        print("Som genérico")

class Cachorro(Animal):
    def buscar(self):
        print(f"{self.nome} está buscando a bola!")

No exemplo acima, Cachorro herda de Animal. Isso significa que um objeto Cachorro terá o atributo nome (inicializado pelo construtor de Animal) e o método fazer_som(), além de seus próprios métodos como buscar(). A herança pode ser múltipla, ou seja, uma classe pode herdar de várias superclasses, mas isso deve ser usado com cuidado.

Também é possível verificar a relação entre classes com a função issubclass() e a relação entre um objeto e uma classe com isinstance():

print(issubclass(Cachorro, Animal))  # True
c = Cachorro("Rex")
print(isinstance(c, Animal))          # True

Essas funções são úteis para validações em tempo de execução.

super()

A função super() é uma ferramenta essencial quando trabalhamos com herança. Ela permite acessar métodos da superclasse a partir da subclasse. O uso mais comum é no método __init__, para inicializar a parte da classe base.

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

class Cachorro(Animal):
    def __init__(self, nome, raca):
        super().__init__(nome)   # chama o __init__ de Animal
        self.raca = raca

Sem a chamada a super(), o atributo nome não seria definido, causando erro. Além disso, super() pode ser usado para chamar qualquer método da superclasse, não apenas o construtor. Isso é útil quando queremos estender o comportamento do método original.

Em herança múltipla, super() segue a MRO, garantindo que cada classe na cadeia seja inicializada corretamente. Por isso, é recomendado usar super() em vez de chamar diretamente a classe base pelo nome, para evitar problemas em hierarquias complexas.

Sobrescrita

Sobrescrita (override) é a capacidade de uma subclasse fornecer uma implementação específica de um método que já existe na superclasse. Em Python, isso é feito simplesmente definindo um método com o mesmo nome na subclasse. O método da subclasse será chamado quando o objeto for da subclasse.

class Animal:
    def fazer_som(self):
        print("Som genérico")

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

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

Quando chamamos fazer_som() em um objeto Cachorro, obtemos "Au au!", e em um Gato, "Miau!". Isso é polimorfismo: o mesmo método se comporta de maneiras diferentes conforme a classe.

Muitas vezes, ao sobrescrever, queremos aproveitar o comportamento da superclasse e adicionar algo. Nesse caso, usamos super():

class Cachorro(Animal):
    def fazer_som(self):
        super().fazer_som()   # chama o método original
        print("Au au!")

Isso é útil quando a superclasse tem uma lógica complexa que não queremos duplicar.

MRO (visão geral)

A MRO (Method Resolution Order) é a ordem em que Python procura por métodos e atributos em uma hierarquia de classes. Em casos de herança múltipla, essa ordem é crucial para evitar ambiguidades. Python usa o algoritmo C3 linearization, que garante que cada classe seja visitada apenas uma vez e que as subclasses apareçam antes das superclasses.

Podemos inspecionar a MRO de uma classe usando o atributo __mro__ ou o método mro():

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

A MRO é importante porque afeta a ordem de chamada de super() e a resolução de métodos. Um padrão comum em herança múltipla é o uso de mixins, que são classes pequenas que fornecem funcionalidades específicas. A MRO garante que os mixins sejam chamados na ordem correta.

Para entender melhor, considere um exemplo de herança em diamante: D herda de B e C, que herdam de A. A MRO lineariza para D, B, C, A, evitando que A seja chamada duas vezes. Isso é essencial para a consistência do estado.

Boas práticas e observações finais

Ao usar herança, siga estas boas práticas:

  • Prefira composição em vez de herança quando a relação não for um "é um" claro.
  • Use super() em vez de chamar a classe base diretamente para garantir consistência.
  • Evite heranças múltiplas profundas; prefira mixins pequenos.
  • Documente a intenção das classes e métodos.

Herança é uma ferramenta poderosa, mas deve ser usada com moderação e clareza. Entender esses conceitos é fundamental para escrever código Python idiomático e eficiente.

Exercícios

  1. Crie uma classe Veiculo com atributos marca e modelo. Depois, crie uma subclasse Carro que adicione o atributo numero_portas. Instancie um objeto e imprima seus atributos.

    ✓ Resposta:
    class Veiculo:
        def __init__(self, marca, modelo):
            self.marca = marca
            self.modelo = modelo
    
    class Carro(Veiculo):
        def __init__(self, marca, modelo, numero_portas):
            super().__init__(marca, modelo)
            self.numero_portas = numero_portas
    
    carro = Carro("Fiat", "Uno", 4)
    print(carro.marca, carro.modelo, carro.numero_portas)
    # Saída: Fiat Uno 4
    
  2. Dada a classe Pessoa com o método apresentar(), crie uma subclasse Estudante que sobrescreva esse método para incluir o curso. Use super() para chamar o método original.

    ✓ Resposta:
    class Pessoa:
        def __init__(self, nome):
            self.nome = nome
    
        def apresentar(self):
            print(f"Olá, eu sou {self.nome}.")
    
    class Estudante(Pessoa):
        def __init__(self, nome, curso):
            super().__init__(nome)
            self.curso = curso
    
        def apresentar(self):
            super().apresentar()
            print(f"Estudo {self.curso}.")
    
    e = Estudante("Maria", "Engenharia")
    e.apresentar()
    # Saída:
    # Olá, eu sou Maria.
    # Estudo Engenharia.
    
  3. Considere a seguinte hierarquia: A é a classe base, B herda de A, e C herda de B. Escreva as classes com um método quem_sou() que imprima o nome da classe. Chame o método em uma instância de C e verifique a MRO com __mro__.

    ✓ Resposta:
    class A:
        def quem_sou(self):
            print("A")
    
    class B(A):
        def quem_sou(self):
            print("B")
    
    class C(B):
        pass
    
    c = C()
    c.quem_sou()  # Saída: B (porque B sobrescreve, e C não)
    print(C.__mro__)
    # (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
    
  4. Explique o que acontece se a classe D herdar de B e C, que herdam de A. Como a MRO resolve a ordem? Escreva um exemplo com um método acao() que chame super().acao() e observe a saída.

    ✓ Resposta:
    class A:
        def acao(self):
            print("A")
    
    class B(A):
        def acao(self):
            print("B")
            super().acao()
    
    class C(A):
        def acao(self):
            print("C")
            super().acao()
    
    class D(B, C):
        def acao(self):
            print("D")
            super().acao()
    
    d = D()
    d.acao()
    # Saída:
    # D
    # B
    # C
    # A
    print(D.__mro__)
    # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
    
  5. Crie uma classe Ponto com coordenadas x e y. Depois, crie uma subclasse Ponto3D que adicione z. Sobrescreva o método distancia_origem para calcular a distância tridimensional. Use super() para reutilizar a lógica bidimensional.

    ✓ Resposta:
    import math
    
    class Ponto:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def distancia_origem(self):
            return math.sqrt(self.x**2 + self.y**2)
    
    class Ponto3D(Ponto):
        def __init__(self, x, y, z):
            super().__init__(x, y)
            self.z = z
    
        def distancia_origem(self):
            dist2d = super().distancia_origem()
            return math.sqrt(dist2d**2 + self.z**2)
    
    p = Ponto3D(3, 4, 12)
    print(p.distancia_origem())  # 13.0
    

Referências