Python Admin

Herança e Polimorfismo em Python: MRO e super() na Prática Já leu

Fundamentos de Herança em Python A herança é um dos pilares da Programação Orientada a Objetos (POO) e permite que uma classe herde atributos e métodos de outra classe, promovendo reutilização de código e criação de hierarquias lógicas. Em Python, quando você cria uma classe que herda de outra, a classe filha (ou subclasse) recebe automaticamente toda a funcionalidade da classe pai (ou superclasse), podendo também estendê-la ou modificá-la conforme necessário. Considere um cenário real: você está desenvolvendo um sistema de gestão de funcionários. Uma classe base poderia conter dados comuns como nome e salário, enquanto classes especializadas como e herdariam dessas informações e adicionariam características próprias. Sem herança, você teria que replicar código em múltiplas classes, violando o princípio DRY (Don't Repeat Yourself). A sintaxe básica é simples: declare a classe pai entre parênteses na definição da classe filha. Todos os atributos e métodos da classe pai estão disponíveis para a filha, mas você pode sobrescrever (override) qualquer método

Fundamentos de Herança em Python

A herança é um dos pilares da Programação Orientada a Objetos (POO) e permite que uma classe herde atributos e métodos de outra classe, promovendo reutilização de código e criação de hierarquias lógicas. Em Python, quando você cria uma classe que herda de outra, a classe filha (ou subclasse) recebe automaticamente toda a funcionalidade da classe pai (ou superclasse), podendo também estendê-la ou modificá-la conforme necessário.

Considere um cenário real: você está desenvolvendo um sistema de gestão de funcionários. Uma classe base Funcionario poderia conter dados comuns como nome e salário, enquanto classes especializadas como Gerente e Desenvolvedor herdariam dessas informações e adicionariam características próprias. Sem herança, você teria que replicar código em múltiplas classes, violando o princípio DRY (Don't Repeat Yourself).

class Funcionario:
    def __init__(self, nome, salario):
        self.nome = nome
        self.salario = salario

    def calcular_bonus(self):
        return self.salario * 0.10

class Gerente(Funcionario):
    def __init__(self, nome, salario, departamento):
        super().__init__(nome, salario)
        self.departamento = departamento

    def calcular_bonus(self):
        return self.salario * 0.20

class Desenvolvedor(Funcionario):
    def __init__(self, nome, salario, linguagem):
        super().__init__(nome, salario)
        self.linguagem = linguagem

    def calcular_bonus(self):
        return self.salario * 0.15

# Testando
gerente = Gerente("Alice", 5000, "TI")
dev = Desenvolvedor("Bob", 4000, "Python")

print(f"{gerente.nome} recebe bônus de R${gerente.calcular_bonus()}")
print(f"{dev.nome} recebe bônus de R${dev.calcular_bonus()}")

A sintaxe básica é simples: declare a classe pai entre parênteses na definição da classe filha. Todos os atributos e métodos da classe pai estão disponíveis para a filha, mas você pode sobrescrever (override) qualquer método para implementar comportamento diferente, como fizemos com calcular_bonus().

Method Resolution Order (MRO) e a Herança Múltipla

O MRO (Ordem de Resolução de Métodos) é um algoritmo que define a sequência em que Python procura por um método ou atributo em uma hierarquia de classes. Isso é especialmente crítico quando você trabalha com herança múltipla, onde uma classe herda de duas ou mais classes pai. Sem um mecanismo bem definido, haveria ambiguidade: qual método chamar primeiro?

Python utiliza o algoritmo C3 Linearization para determinar o MRO, garantindo que: (1) classes filhas sejam consultadas antes das pai; (2) a ordem de herança declarada seja respeitada; (3) cada classe apareça apenas uma vez na sequência. Você pode visualizar o MRO de qualquer classe usando o método mro() ou o atributo __mro__.

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

class Terrestre:
    def mover(self):
        return "Andando"

class Aquatico:
    def mover(self):
        return "Nadando"

class Anfibio(Terrestre, Aquatico, Animal):
    pass

# Visualizando o MRO
print(Anfibio.mro())
# Saída: [<class 'Anfibio'>, <class 'Terrestre'>, <class 'Aquatico'>, <class 'Animal'>, <class 'object'>]

# Ou usando __mro__
for classe in Anfibio.__mro__:
    print(classe.__name__)

# Testando a resolução
anf = Anfibio()
print(anf.mover())  # Saída: Andando (encontrado em Terrestre primeiro)
print(anf.fazer_som())  # Saída: Som genérico

Observe que mover() retorna "Andando" porque Terrestre aparece antes de Aquatico na herança. Se a ordem fosse invertida (class Anfibio(Aquatico, Terrestre, Animal)), o resultado seria "Nadando". O MRO garante previsibilidade e segurança em hierarquias complexas, evitando o problema do "diamante" (diamond problem) que outras linguagens enfrentam.

Evitando Armadilhas com MRO

Um erro comum é criar hierarquias que violam o MRO, resultando em exceções. Por exemplo, se você tiver uma estrutura circular ou uma ordem inconsistente nas heranças, Python levantará um TypeError. Sempre verifique o MRO ao trabalhar com herança múltipla e garanta que a ordem das classes pai faz sentido semanticamente para seu domínio.

# Exemplo que gera erro (comentado para não quebrar)
# class A(B, C): pass
# class B(C): pass
# class C(A): pass  # Circular - TypeError!

# Ordem correta
class Veiculo:
    def ligar(self):
        return "Ligando motor"

class Terrestre(Veiculo):
    def andar(self):
        return "Andando na terra"

class Aquatico(Veiculo):
    def nadar(self):
        return "Nadando na água"

class Carro(Terrestre):
    pass

class Barco(Aquatico):
    pass

print(Carro.mro())
print(Barco.mro())

A Função super() e Chamadas Cooperativas

A função super() é o coração da reutilização de código eficiente em Python. Ela retorna um objeto proxy que delega chamadas de métodos a uma classe pai ou irmã na cadeia de herança, seguindo o MRO. Diferentemente de chamar a classe pai diretamente pelo nome (Funcionario.__init__(self, ...)), super() é dinamicamente resolvido, tornando seu código mais flexível e seguro para refatorações futuras.

O grande benefício de usar super() transparece em cenários de herança múltipla ou profunda. Se você usar super(), cada classe colabora automaticamente sem conhecer exatamente quem vem a seguir no MRO. Isso é especialmente valioso quando você estende uma hierarquia existente sem quebrar código que já funciona.

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
        print(f"Pessoa.__init__ chamado para {self.nome}")

    def apresentar(self):
        return f"Olá, meu nome é {self.nome}"

class Estudante(Pessoa):
    def __init__(self, nome, idade, matricula):
        super().__init__(nome, idade)
        self.matricula = matricula
        print(f"Estudante.__init__ chamado para {self.nome}")

    def apresentar(self):
        pai = super().apresentar()
        return f"{pai} e sou estudante com matrícula {self.matricula}"

class Bolsista(Estudante):
    def __init__(self, nome, idade, matricula, valor_bolsa):
        super().__init__(nome, idade, matricula)
        self.valor_bolsa = valor_bolsa
        print(f"Bolsista.__init__ chamado para {self.nome}")

    def apresentar(self):
        pai = super().apresentar()
        return f"{pai}. Bolsa: R${self.valor_bolsa}"

# Testando a cadeia
bolsista = Bolsista("Carlos", 20, "2024001", 1500)
print(bolsista.apresentar())

# Saída do __init__:
# Pessoa.__init__ chamado para Carlos
# Estudante.__init__ chamado para Carlos
# Bolsista.__init__ chamado para Carlos

# Saída do apresentar():
# Olá, meu nome é Carlos e sou estudante com matrícula 2024001. Bolsa: R$1500

Neste exemplo, note como super() encadeia as chamadas respeitando o MRO. Sem super(), você teria que chamar Pessoa.__init__(self, ...) em Estudante, depois Estudante.__init__(self, ...) em Bolsista, criando dependências rígidas. Com super(), cada classe sabe apenas que deve chamar o próximo na fila, promovendo desacoplamento.

Sintaxe Moderna vs. Clássica

Em Python 3, você pode chamar super() sem argumentos dentro de um método (forma moderna). Python automaticamente deduz a classe e a instância. Na forma clássica, você passa a classe e a instância explicitamente. Ambas funcionam, mas a forma moderna é mais legível e recomendada.

# Forma clássica (ainda funciona, mas evite)
class Pai:
    def metodo(self):
        return "Pai"

class Filho(Pai):
    def metodo(self):
        return super(Filho, self).metodo() + " + Filho"

# Forma moderna (Python 3, recomendada)
class Pai2:
    def metodo(self):
        return "Pai"

class Filho2(Pai2):
    def metodo(self):
        return super().metodo() + " + Filho"

f = Filho2()
print(f.metodo())  # Saída: Pai + Filho

Polimorfismo: Comportamentos Diferentes, Interface Comum

Polimorfismo significa "muitas formas" e, em POO, refere-se à capacidade de objetos de diferentes classes responderem ao mesmo método com comportamentos distintos. É a consecução natural da herança: quando você define um método em uma classe base e sobrescreve em subclasses, cada uma implementa sua própria versão. Quem chama não precisa saber a classe exata, apenas que aquele método existe.

Polimorfismo torna seu código mais genérico e reutilizável. Em vez de escrever lógicas diferentes para cada tipo de objeto, você escreve uma única função que funciona com qualquer objeto que implemente a interface esperada. Isso é uma aplicação prática do princípio SOLID chamado "Liskov Substitution Principle": objetos de subclasses devem poder substituir objetos de classes pai sem quebrar a aplicação.

from abc import ABC, abstractmethod

class Pagamento(ABC):
    @abstractmethod
    def processar(self, valor):
        pass

    @abstractmethod
    def validar(self):
        pass

class PagamentoCartao(Pagamento):
    def __init__(self, numero, cvv):
        self.numero = numero
        self.cvv = cvv

    def processar(self, valor):
        if self.validar():
            return f"Processando R${valor} via Cartão {self.numero[-4:]}"
        return "Cartão inválido"

    def validar(self):
        return len(self.numero) == 16 and len(self.cvv) == 3

class PagamentoBoleto(Pagamento):
    def __init__(self, codigo):
        self.codigo = codigo

    def processar(self, valor):
        if self.validar():
            return f"Processando R${valor} via Boleto {self.codigo}"
        return "Boleto inválido"

    def validar(self):
        return len(self.codigo) == 47

class PagamentoPix(Pagamento):
    def __init__(self, chave):
        self.chave = chave

    def processar(self, valor):
        if self.validar():
            return f"Processando R${valor} via Pix {self.chave}"
        return "Chave Pix inválida"

    def validar(self):
        return "@" in self.chave

# Função polimórfica
def processar_compra(pagamento, valor):
    return pagamento.processar(valor)

# Testando polimorfismo
cartao = PagamentoCartao("1234567890123456", "123")
boleto = PagamentoBoleto("12345678901234567890123456789012345678901234567")
pix = PagamentoPix("usuario@pix")

for metodo in [cartao, boleto, pix]:
    print(processar_compra(metodo, 100))

Perceba que processar_compra() não precisa saber qual tipo de pagamento está recebendo. Ela apenas chama processar(), e cada subclasse faz sua própria coisa. Se no futuro você adicionar PagamentoApplePay, a função continua funcionando sem modificação. Isso é o verdadeiro poder do polimorfismo.

Uso de Classes Abstratas

No exemplo acima, usamos ABC (Abstract Base Class) e @abstractmethod para forçar que qualquer subclasse implemente certos métodos. Isso garante que a interface é respeitada e evita instanciação acidental de classes incompletas.

# Isto levanta TypeError
# pagamento = Pagamento("algo")  # TypeError: Can't instantiate abstract class

Padrões e Boas Práticas

Ao trabalhar com herança e polimorfismo, algumas práticas minimizam bugs e melhoram manutenibilidade. Primeiro, prefira composição à herança quando a relação não for claramente "é um" (is-a). Por exemplo, se uma classe Carro precisa de um Motor, é melhor ter um atributo que referencie a classe Motor do que herdar dela. Segundo, sempre use super() em vez de chamar a classe pai pelo nome direto—garante que mudanças futuras na hierarquia não quebrem seu código. Terceiro, documente o contrato esperado de uma classe base, deixando claro quais métodos subclasses devem (ou podem) sobrescrever.

class Veiculo:
    """Classe base para todos os veículos.

    Subclasses devem implementar:
    - ligar(): inicia o motor
    - desligar(): desliga o motor
    - dirigir(distancia): realiza deslocamento
    """

    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"

    def desligar(self):
        self._ligado = False
        return f"{self.marca} {self.modelo} desligado"

    def dirigir(self, distancia):
        if not self._ligado:
            raise RuntimeError("Veículo desligado!")
        return f"Dirigindo {distancia}km"

class Carro(Veiculo):
    def __init__(self, marca, modelo, portas):
        super().__init__(marca, modelo)
        self.portas = portas

    def abrir_porta(self):
        return f"Abrindo {self.portas} portas"

class Moto(Veiculo):
    def __init__(self, marca, modelo, cilindrada):
        super().__init__(marca, modelo)
        self.cilindrada = cilindrada

    def fazer_manobra(self):
        if not self._ligado:
            raise RuntimeError("Moto desligada!")
        return "Fazendo manobra!"

# Testando
carro = Carro("Toyota", "Corolla", 4)
moto = Moto("Honda", "CB500", 500)

print(carro.ligar())
print(carro.abrir_porta())
print(carro.dirigir(10))

print(moto.ligar())
print(moto.fazer_manobra())
print(moto.dirigir(50))

Outra prática valiosa é manter hierarquias rasas. Hierarquias muito profundas (muitos níveis de herança) tornam o código difícil de entender e modificar. Se você se vê criando uma cadeia com mais de 3 ou 4 níveis, reconsidere se composição não seria mais adequada. Finalmente, teste suas hierarquias. Use ferramentas como unittest para validar que subclasses realmente funcionam como esperado e que o MRO não causa surpresas.

import unittest

class TestVeiculo(unittest.TestCase):
    def test_carro_dirigir_desligado(self):
        carro = Carro("Toyota", "Corolla", 4)
        with self.assertRaises(RuntimeError):
            carro.dirigir(10)

    def test_carro_dirigir_ligado(self):
        carro = Carro("Toyota", "Corolla", 4)
        carro.ligar()
        resultado = carro.dirigir(10)
        self.assertIn("Dirigindo", resultado)

    def test_moto_manobra_desligada(self):
        moto = Moto("Honda", "CB500", 500)
        with self.assertRaises(RuntimeError):
            moto.fazer_manobra()

if __name__ == "__main__":
    unittest.main()

Conclusão

Dominar herança e polimorfismo em Python repousa em três pilares bem compreendidos: (1) MRO governa a resolução de métodos — use mro() para visualizar a ordem e evitar armadilhas em herança múltipla; (2) super() é essencial para código cooperativo — sempre prefira super() a chamadas diretas de classe pai, garantindo flexibilidade e manutenibilidade; (3) Polimorfismo torna seu código genérico e extensível — implemente interfaces esperadas em subclasses e deixe que objetos respondam ao mesmo método com comportamentos próprios, respeitando o Liskov Substitution Principle.

Esses conceitos, quando bem aplicados, transformam código repetitivo em hierarquias limpas e reutilizáveis. A prática consistente — escrevendo pequenos projetos com herança múltipla, testando MRO e refatorando para usar super() — consolidará seu entendimento.

Referências


Artigos relacionados