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.