Nesta aula, exploraremos dois tópicos avançados de orientação a objetos em Python: herança múltipla e mixins. A herança múltipla permite que uma classe derive de mais de uma classe base, combinando comportamentos. Mixins são classes projetadas para fornecer funcionalidades específicas e reutilizáveis, sem a intenção de serem instanciadas sozinhas. Compreender esses conceitos e como o Python resolve conflitos de nomes através do MRO (Method Resolution Order) é fundamental para escrever código modular e eficiente.

Vamos começar com um exemplo simples para motivar o uso de herança múltipla, depois mergulharemos nos detalhes do MRO e finalizaremos com boas práticas para evitar armadilhas comuns.

Herança múltipla

Herança múltipla é a capacidade de uma classe herdar atributos e métodos de mais de uma classe pai. Em Python, isso é feito listando as classes base na definição da classe, separadas por vírgulas. Por exemplo, uma classe Gato pode herdar de Mamifero e AnimalDeEstimacao, combinando características de ambas.

Considere o seguinte exemplo:

class Mamifero:
    def __init__(self, nome):
        self.nome = nome
    def amamentar(self):
        print(f"{self.nome} está amamentando.")

class AnimalDeEstimacao:
    def __init__(self, dono):
        self.dono = dono
    def brincar(self):
        print(f"{self.nome} está brincando com {self.dono}.")

class Gato(Mamifero, AnimalDeEstimacao):
    def __init__(self, nome, dono):
        Mamifero.__init__(self, nome)
        AnimalDeEstimacao.__init__(self, dono)
    def miar(self):
        print(f"{self.nome} diz: Miau!")

gato = Gato("Bichano", "Ana")
gato.amamentar()
gato.brincar()
gato.miar()

Neste código, Gato herda de Mamifero e AnimalDeEstimacao. Observe que precisamos chamar explicitamente os construtores das classes base. Python não chama automaticamente os __init__ das classes pais em herança múltipla, a menos que usemos super() com cuidado. Isso nos leva ao próximo tópico: o MRO.

Mixins

Mixins são classes que fornecem funcionalidades específicas e são projetadas para serem combinadas com outras classes via herança múltipla. Elas não devem ser instanciadas sozinhas, mas sim usadas para adicionar comportamentos a outras classes. Por exemplo, podemos criar um mixin SerializavelMixin que adiciona métodos de serialização JSON a qualquer classe.

import json

class SerializavelMixin:
    def para_json(self):
        return json.dumps(self.__dict__)

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

class PessoaSerializavel(Pessoa, SerializavelMixin):
    pass

p = PessoaSerializavel("João", 30)
print(p.para_json())  # {"nome": "João", "idade": 30}

Mixins são úteis para reutilizar funcionalidades transversais, como logging, validação, ou formatação. Eles geralmente não possuem estado próprio (apenas métodos) e devem ser pequenos e focados. Uma boa prática é nomear mixins com o sufixo Mixin para indicar sua intenção.

MRO em profundidade

MRO (Method Resolution Order) é a ordem em que Python procura métodos e atributos em uma hierarquia de classes. Em herança múltipla, o MRO segue o algoritmo C3 Linearization, que garante uma ordem consistente e respeita a precedência das classes base. Podemos visualizar o MRO de uma classe usando o atributo especial __mro__ ou o método mro().

Exemplo:

class A:
    def metodo(self):
        print("A")

class B(A):
    def metodo(self):
        print("B")

class C(A):
    def metodo(self):
        print("C")

class D(B, C):
    pass

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

d = D()
d.metodo()  # B

O MRO de D é D -> B -> C -> A -> object. Quando chamamos d.metodo(), Python procura primeiro em D, depois B (encontra), e executa. Se B não tivesse o método, iria para C, depois A. O algoritmo C3 garante que as classes base sejam visitadas na ordem de declaração e que subclasses tenham precedência sobre superclasses.

Entender o MRO é crucial para usar super() corretamente. Em herança múltipla, super() segue o MRO, permitindo chamadas cooperativas. Exemplo:

class A:
    def __init__(self):
        print("A.__init__")
        super().__init__()

class B:
    def __init__(self):
        print("B.__init__")
        super().__init__()

class C(A, B):
    def __init__(self):
        print("C.__init__")
        super().__init__()

c = C()
# C.__init__
# A.__init__
# B.__init__

Note que super().__init__() em A chama B.__init__ porque o MRO de C é C -> A -> B -> object. Isso permite que todos os construtores sejam executados na ordem correta, desde que todas as classes usem super() de forma consistente.

Boas práticas

Ao usar herança múltipla e mixins, algumas boas práticas ajudam a evitar problemas:

  • Prefira composição à herança quando possível. Herança múltipla pode tornar o código complexo e difícil de depurar.
  • Use mixins para funcionalidades pequenas e específicas. Evite mixins que dependam de estado ou que exijam inicialização complexa.
  • Nomeie mixins com o sufixo Mixin para deixar clara a intenção.
  • Evite diamantes problemáticos: quando duas classes base compartilham uma superclasse comum, o MRO resolve, mas pode causar comportamentos inesperados se não for bem compreendido.
  • Sempre use super() nos construtores para garantir que todos os __init__ sejam chamados na ordem do MRO.
  • Documente o MRO esperado em classes com herança múltipla complexa.

Exemplo de um mixin bem projetado:

class LogavelMixin:
    def log(self, mensagem):
        print(f"[LOG] {self.__class__.__name__}: {mensagem}")

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

class Carro(Veiculo, LogavelMixin):
    def __init__(self, modelo):
        super().__init__(modelo)
        self.log("Carro criado")

carro = Carro("Fusca")
# [LOG] Carro: Carro criado

Referências

Exercícios

  1. Crie uma classe Animal com um método falar que retorna uma string genérica. Crie duas classes Cachorro e Gato que herdam de Animal e sobrescrevem falar para retornar "Au Au" e "Miau", respectivamente. Depois, crie uma classe CachorroGato que herda de Cachorro e Gato. Qual será a saída de CachorroGato().falar()? Explique com base no MRO.

    ✓ Resposta: A saída será "Au Au" porque o MRO de CachorroGato é: CachorroGato -> Cachorro -> Gato -> Animal -> object. Como Cachorro é a primeira classe base listada, seu método falar é encontrado primeiro. O MRO segue a ordem de declaração das classes base.
  2. Crie um mixin AndarMixin que adiciona um método andar que imprime "Andando...". Crie uma classe Pessoa que herda de AndarMixin e tem um atributo nome. Instancie uma pessoa e chame o método andar.

    ✓ Resposta:
    class AndarMixin:
        def andar(self):
            print("Andando...")
    
    class Pessoa(AndarMixin):
        def __init__(self, nome):
            self.nome = nome
    
    p = Pessoa("Maria")
    p.andar()  # Andando...
  3. Explique o que é o algoritmo C3 Linearization e como ele determina o MRO em Python. Dê um exemplo com três classes onde o MRO não é trivial.

    ✓ Resposta: O C3 Linearization é um algoritmo que produz uma ordem linear das classes base, garantindo que subclasses precedam superclasses e que a ordem de declaração seja respeitada. Exemplo: class A: pass, class B(A): pass, class C(A): pass, class D(B, C): pass. O MRO de D é D -> B -> C -> A -> object. O algoritmo resolve o diamante garantindo que A seja visitado depois de B e C.
  4. Considere as classes:

    class X:
        def metodo(self):
            print("X")
    
    class Y:
        def metodo(self):
            print("Y")
    
    class Z(X, Y):
        pass
    
    class W(Y, X):
        pass

    Qual é a saída de Z().metodo() e W().metodo()? Explique.

    ✓ Resposta: Z().metodo() imprime "X" porque o MRO de Z é Z -> X -> Y -> object. W().metodo() imprime "Y" porque o MRO de W é W -> Y -> X -> object. A ordem de declaração das classes base determina qual método é encontrado primeiro.
  5. Escreva um mixin JSONMixin que adiciona um método to_json que retorna uma string JSON dos atributos da instância. Crie uma classe Produto com atributos nome e preco que utiliza o mixin. Teste a serialização.

    ✓ Resposta:
    import json
    
    class JSONMixin:
        def to_json(self):
            return json.dumps(self.__dict__)
    
    class Produto(JSONMixin):
        def __init__(self, nome, preco):
            self.nome = nome
            self.preco = preco
    
    p = Produto("Caneta", 1.50)
    print(p.to_json())  # {"nome": "Caneta", "preco": 1.5}