No artigo Classes e Objetos: os fundamentos da Orientação a Objetos criamos classes independentes. Mas e quando duas classes compartilham características comuns? Reescrever o mesmo código em cada uma viola um princípio fundamental do desenvolvimento de software: DRY — Don't Repeat Yourself. Herança resolve esse problema permitindo que uma classe herde atributos e métodos de outra, e polimorfismo permite que objetos de tipos diferentes respondam ao mesmo comando de formas distintas.
Herança Simples
Uma classe filha herda tudo da classe pai e pode estender ou modificar esse comportamento:
class Animal:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def apresentar(self):
return f"Eu sou {self.nome} e tenho {self.idade} ano(s)."
def emitir_som(self):
return "..."
class Cachorro(Animal):
def emitir_som(self):
return "Au au!"
def buscar(self, objeto):
return f"{self.nome} buscou o {objeto}!"
class Gato(Animal):
def emitir_som(self):
return "Miau!"
def arranhar(self):
return f"{self.nome} arranhou o sofá."
rex = Cachorro("Rex", 3)
mimi = Gato("Mimi", 5)
print(rex.apresentar()) # Eu sou Rex e tenho 3 ano(s).
print(rex.emitir_som()) # Au au!
print(rex.buscar("graveto")) # Rex buscou o graveto!
print(mimi.emitir_som()) # Miau!
Cachorro e Gato herdam apresentar() de Animal sem precisar reescrevê-lo. Cada uma sobrescreve emitir_som() com seu próprio comportamento — isso é sobrescrita de método (method overriding).
A Função super()
super() permite chamar métodos da classe pai a partir da classe filha — essencial para estender o construtor:
class Veiculo:
def __init__(self, marca, modelo, ano):
self.marca = marca
self.modelo = modelo
self.ano = ano
def descricao(self):
return f"{self.ano} {self.marca} {self.modelo}"
class Carro(Veiculo):
def __init__(self, marca, modelo, ano, portas):
super().__init__(marca, modelo, ano) # chama __init__ do pai
self.portas = portas
def descricao(self):
base = super().descricao() # reutiliza o método do pai
return f"{base} ({self.portas} portas)"
class CarroEletrico(Carro):
def __init__(self, marca, modelo, ano, portas, autonomia_km):
super().__init__(marca, modelo, ano, portas)
self.autonomia_km = autonomia_km
def descricao(self):
base = super().descricao()
return f"{base} — Elétrico, {self.autonomia_km}km de autonomia"
civic = Carro("Honda", "Civic", 2023, 4)
model3 = CarroEletrico("Tesla", "Model 3", 2024, 4, 560)
print(civic.descricao())
# 2023 Honda Civic (4 portas)
print(model3.descricao())
# 2024 Tesla Model 3 (4 portas) — Elétrico, 560km de autonomia
Polimorfismo
Polimorfismo significa "muitas formas". Em Python, objetos de classes diferentes podem ser tratados de forma uniforme se implementarem os mesmos métodos:
class Circulo:
def __init__(self, raio):
self.raio = raio
def area(self):
import math
return math.pi * self.raio ** 2
def perimetro(self):
import math
return 2 * math.pi * self.raio
class Retangulo:
def __init__(self, largura, altura):
self.largura = largura
self.altura = altura
def area(self):
return self.largura * self.altura
def perimetro(self):
return 2 * (self.largura + self.altura)
class Triangulo:
def __init__(self, base, altura, lado_a, lado_b, lado_c):
self.base = base
self.altura = altura
self.lados = (lado_a, lado_b, lado_c)
def area(self):
return (self.base * self.altura) / 2
def perimetro(self):
return sum(self.lados)
# Polimorfismo em ação — mesma interface, comportamentos diferentes
formas = [
Circulo(5),
Retangulo(4, 6),
Triangulo(3, 4, 3, 4, 5),
]
for forma in formas:
nome = type(forma).__name__
print(f"{nome:12} | Área: {forma.area():7.2f} | Perímetro: {forma.perimetro():.2f}")
Saída:
Circulo | Área: 78.54 | Perímetro: 31.42
Retangulo | Área: 24.00 | Perímetro: 20.00
Triangulo | Área: 6.00 | Perímetro: 12.00
Nenhuma das classes herda da outra — mesmo assim podemos tratá-las uniformemente. Isso é o duck typing do Python: se o objeto tem o método area(), ele pode ser usado onde area() for chamado. "Se anda como pato e grasna como pato, é um pato."
Classes Abstratas
Quando você quer garantir que subclasses implementem determinados métodos, use o módulo abc:
from abc import ABC, abstractmethod
class Forma(ABC):
"""Classe base abstrata — não pode ser instanciada diretamente.
A checagem acontece na INSTANCIAÇÃO, não na definição: uma subclasse
que esqueça de implementar um método abstrato é criada sem erro, e só
levanta TypeError quando alguém tenta construir um objeto dela.
"""
@abstractmethod
def area(self):
"""Toda forma deve implementar area()."""
pass
@abstractmethod
def perimetro(self):
"""Toda forma deve implementar perimetro()."""
pass
def descricao(self):
"""Método concreto — herdado por todas as subclasses."""
return (f"{type(self).__name__}: "
f"área={self.area():.2f}, "
f"perímetro={self.perimetro():.2f}")
class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado
def area(self):
return self.lado ** 2
def perimetro(self):
return 4 * self.lado
class Hexagono(Forma):
def __init__(self, lado):
self.lado = lado
def area(self):
import math
return (3 * math.sqrt(3) / 2) * self.lado ** 2
def perimetro(self):
return 6 * self.lado
# Forma() # TypeError — não pode instanciar classe abstrata
q = Quadrado(5)
h = Hexagono(4)
print(q.descricao()) # Quadrado: área=25.00, perímetro=20.00
print(h.descricao()) # Hexagono: área=41.57, perímetro=24.00
Herança Múltipla e MRO
Python suporta herança múltipla — uma classe pode herdar de várias classes ao mesmo tempo:
class Nadador:
def nadar(self):
return f"{self.nome} está nadando."
class Corredor:
def correr(self):
return f"{self.nome} está correndo."
class Ciclista:
def pedalar(self):
return f"{self.nome} está pedalando."
class Triatleta(Nadador, Corredor, Ciclista):
def __init__(self, nome):
self.nome = nome
def competir(self):
return [self.nadar(), self.pedalar(), self.correr()]
atleta = Triatleta("Carlos")
for atividade in atleta.competir():
print(atividade)
# Carlos está nadando.
# Carlos está pedalando.
# Carlos está correndo.
Quando há ambiguidade sobre qual método usar, Python segue o MRO — Method Resolution Order, calculado pelo algoritmo C3:
print(Triatleta.__mro__)
# (<class '__main__.Triatleta'>, <class '__main__.Nadador'>,
# <class '__main__.Corredor'>, <class '__main__.Ciclista'>,
# <class 'object'>)
Só que no exemplo acima o MRO não decide nada: Nadador, Corredor e Ciclista não têm método algum em comum, então não há ambiguidade a resolver. A ordem importa quando duas classes da hierarquia definem o mesmo método — a situação que se chama diamante, porque dois caminhos partem de uma base comum e voltam a se encontrar:
class A:
def quem(self): return "A"
class B(A):
def quem(self): return "B -> " + super().quem()
class C(A):
def quem(self): return "C -> " + super().quem()
class D(B, C):
pass
print(D().quem()) # B -> C -> A
print([k.__name__ for k in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']
Este é o ponto que quase todo material erra ao explicar super(): lendo a classe B isoladamente, é natural concluir que super().quem() chama A.quem, já que B herda de A. Não é o que acontece. O super() não significa a classe pai; significa o próximo da fila do MRO do objeto em questão. Quando quem está sendo usado é um D, o próximo depois de B é C, e por isso A é visitada uma única vez, no fim. É essa propriedade que faz a herança múltipla funcionar de forma cooperativa em Python, e é por isso que um método que participa de uma hierarquia deve chamar super() mesmo parecendo não ter para onde ir.
O algoritmo que calcula essa ordem, o C3, também sabe recusar o impossível. Se duas bases exigirem ordens contraditórias, o erro aparece na definição da classe, não no uso:
class X(B, C): pass
class Y(C, B): pass
class Z(X, Y): pass
# TypeError: Cannot create a consistent method resolution order (MRO)
# for bases B, C
Vale notar ainda que os três papéis do exemplo do triatleta usam self.nome sem nunca declará-lo — eles contam com um __init__ que só existe na classe de baixo. Instanciar Nadador() sozinho e chamar nadar() levanta AttributeError. É o padrão mixin, legítimo e muito usado, mas com um contrato implícito que precisa estar documentado: a classe não funciona sozinha, e espera que quem a combine forneça certos atributos.
Verificando Herança
rex = Cachorro("Rex", 3)
print(isinstance(rex, Cachorro)) # True
print(isinstance(rex, Animal)) # True — herança
print(isinstance(rex, Gato)) # False
print(issubclass(Cachorro, Animal)) # True
print(issubclass(Gato, Cachorro)) # False
Exemplo Completo: Sistema de Pagamentos
from abc import ABC, abstractmethod
class MetodoPagamento(ABC):
def __init__(self, titular):
self.titular = titular
@abstractmethod
def processar(self, valor):
pass
@abstractmethod
def descricao(self):
pass
def recibo(self, valor, status):
# recebe o status pronto: emitir recibo não pode processar pagamento
return (f"Recibo — {self.titular}\n"
f"Método: {self.descricao()}\n"
f"Valor: R${valor:.2f}\n"
f"Status: {status}")
class CartaoCredito(MetodoPagamento):
def __init__(self, titular, numero, limite):
super().__init__(titular)
self.numero = f"**** **** **** {numero[-4:]}"
self.limite = limite
self._gasto = 0
def processar(self, valor):
if self._gasto + valor > self.limite:
return "RECUSADO — limite insuficiente"
self._gasto += valor
return "APROVADO"
def descricao(self):
return f"Cartão de Crédito {self.numero}"
class Pix(MetodoPagamento):
def __init__(self, titular, chave):
super().__init__(titular)
self.chave = chave
def processar(self, valor):
return "APROVADO — transferência instantânea"
def descricao(self):
return f"Pix ({self.chave})"
class Boleto(MetodoPagamento):
def __init__(self, titular):
super().__init__(titular)
def processar(self, valor):
return "PENDENTE — aguardando compensação"
def descricao(self):
return "Boleto Bancário"
pagamentos = [
CartaoCredito("Ana", "1234567890123456", 2000),
Pix("Bruno", "bruno@email.com"),
Boleto("Carla"),
]
valores = [150.00, 89.90, 320.00]
for metodo, valor in zip(pagamentos, valores):
status = metodo.processar(valor) # cobra uma vez, aqui
print(metodo.recibo(valor, status)) # e só formata
print("-" * 40)
A separação acima não é preciosismo. Na versão natural de escrever essa classe, o recibo monta o texto chamando self.processar(valor) lá dentro — e como processar do cartão soma ao _gasto, emitir o recibo cobra o cartão. Reimprimir, mandar por e-mail ou registrar no log, cada um custa uma cobrança nova:
c = CartaoCredito("Ana", "1234567890123456", 2000)
c.recibo(150.00) # APROVADO — _gasto = 150
c.recibo(150.00) # APROVADO — _gasto = 300, cobrou de novo
c.recibo(1800.00) # RECUSADO — o limite estourou por causa da reimpressão
A regra que evita a classe inteira desse problema: um método ou consulta ou altera, nunca as duas coisas. Quem tem nome de consulta — recibo, descricao, total, qualquer @property — deve poder ser chamado dez vezes seguidas sem mudar nada. Quando o nome promete uma coisa e o corpo faz outra, o defeito não é encontrável por leitura de quem chama, porque quem chama está confiando no nome.
Herança e polimorfismo resolvem problemas distintos, e em Python o segundo raramente depende do primeiro. A herança serve para reaproveitar implementação, e é uma ferramenta a usar com parcimônia: cada nível novo amarra a filha à forma interna da mãe. O polimorfismo, esse sim, é o que se busca o tempo todo — e o duck typing o entrega sem hierarquia nenhuma, bastando que os objetos respondam aos mesmos métodos, como as três formas geométricas deste artigo, que não herdam umas das outras e ainda assim são tratadas pelo mesmo laço. Quando o contrato precisa ser garantido em vez de combinado, o ABC com @abstractmethod obriga a implementação — lembrando que a checagem ocorre na instanciação, não na definição da subclasse.
Duas correções de entendimento ficam deste artigo. O super() não significa a classe pai, e sim o próximo da fila do MRO do objeto: numa hierarquia em diamante, o super() escrito dentro de uma classe pode levar a uma irmã dela, não à mãe, e é essa propriedade que faz a herança múltipla funcionar de forma cooperativa. E um método que consulta não pode alterar: o recibo do exemplo de pagamentos processava a cobrança ao montar o texto, de modo que reimprimir um recibo cobrava o cartão outra vez — defeito impossível de achar lendo quem chama, porque quem chama está confiando no nome do método.
Fontes e leituras recomendadas
- Herança — documentação oficial — https://docs.python.org/3/tutorial/classes.html#inheritance
- Módulo abc — classes abstratas — https://docs.python.org/3/library/abc.html
- MRO e algoritmo C3 — https://docs.python.org/3/howto/mro.html
- Duck typing e protocolos — https://docs.python.org/3/glossary.html#term-duck-typing
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 14 — herança, para o bem e para o mal.
- PHILLIPS, Dusty; LOTT, Steven F. Python Object-Oriented Programming. 4. ed. Packt, 2021. Cap. 4–5 — herança e polimorfismo com exemplos avançados.
- GAMMA, Erich et al. Design Patterns. Addison-Wesley, 1994. — a base teórica por trás do uso de herança e polimorfismo em sistemas reais.
Exercícios
Exercício 1
Um sistema de faturamento tem um método fatura(valor) que monta o texto da fatura e, ao montá-lo, consulta self.processar(valor) para incluir o status. O time de suporte reclama que alguns clientes foram cobrados duas e três vezes, sempre os que ligaram pedindo segunda via. Explique a relação e enuncie a regra de projeto que evita esta classe inteira de defeitos.
Ver resposta
✓ Resposta: O método processar não apenas informa um status: ele altera estado, somando o valor ao gasto acumulado do cartão. Como o fatura o chama para preencher a linha de status, cada emissão de fatura executa uma cobrança nova. Quem pediu segunda via recebeu, junto com o documento, mais um débito — e a terceira ligação gerou a terceira cobrança. Com um cartão de limite 2000 e uma compra de 150 reimpressa uma vez, o gasto vai a 300, e uma compra legítima de 1800 que deveria caber passa a ser recusada por limite insuficiente, o que espalha o sintoma para transações sem relação nenhuma com a segunda via. A regra que evita a classe inteira é a separação entre comando e consulta: um método ou responde alguma coisa ou muda alguma coisa, nunca as duas. Tudo que tem nome de consulta — fatura, recibo, descricao, total, e sobretudo qualquer @property — precisa poder ser chamado dez vezes seguidas sem consequência. O motivo de esse defeito ser tão caro é que ele é invisível do lado de quem chama: o código que emite a segunda via está correto e legível, e confia no nome do método, que é exatamente o contrato quebrado. A correção é processar uma vez, no ponto onde a decisão de cobrar é tomada, e passar o resultado adiante: status = metodo.processar(valor) seguido de metodo.fatura(valor, status). Vale acrescentar que, em sistemas de pagamento reais, a proteção não para aí — a operação recebe uma chave de idempotência, de modo que a mesma requisição repetida por reenvio de rede ou por clique duplo seja reconhecida e não cobre de novo.
Exercício 2
Dado o código abaixo, diga o que D().quem() imprime e justifique. A resposta intuitiva — que super() dentro de B chama A, já que B herda de A — está errada.
class A:
def quem(self): return "A"
class B(A):
def quem(self): return "B -> " + super().quem()
class C(A):
def quem(self): return "C -> " + super().quem()
class D(B, C):
pass
Ver resposta
✓ Resposta: Imprime B -> C -> A. A chave é que super() não significa a classe pai da classe onde estou escrevendo; significa o próximo da fila do MRO do objeto que está sendo usado. O MRO de D é [D, B, C, A, object], calculado pelo algoritmo C3, que garante que uma classe apareça sempre antes de suas bases e que a ordem das bases na declaração seja respeitada. Chamando D().quem(), a busca começa em D, que não define o método, e encontra B.quem. Dentro de B, o super() consulta a posição de B no MRO do objeto, que é um D, e o próximo da fila ali é C — não A. Por isso a cadeia continua em C.quem, cujo super() finalmente alcança A. O resultado é que A é visitada uma única vez, apesar de haver dois caminhos até ela, que é precisamente o problema que o diamante cria em linguagens sem MRO. Note que a mesma classe B se comporta de forma diferente conforme o objeto: B().quem() devolve B -> A, porque no MRO de B o próximo é mesmo A. Daí a consequência prática que vale carregar: um método que participa de uma hierarquia deve chamar super() mesmo quando parece não haver ninguém adiante, porque não é a classe que decide quem vem depois — é o objeto. Isso vale especialmente para __init__: uma classe que inicializa seus atributos e não chama super().__init__() interrompe a cadeia e deixa as demais bases sem inicializar, o que na herança múltipla produz atributos faltando sem erro imediato. Vale saber, por fim, que o C3 também recusa o impossível: bases que exigem ordens contraditórias produzem TypeError: Cannot create a consistent method resolution order já na definição da classe.
Exercício 3
Uma classe base define @abstractmethod def salvar(self). Um desenvolvedor cria uma subclasse e esquece de implementar o método. O código passa na importação, passa na revisão, passa no linter, e só quebra em produção. Explique em que momento a verificação acontece e que prática de teste teria pegado isso antes.
Ver resposta
✓ Resposta: O @abstractmethod não é verificado quando a subclasse é definida, e sim quando alguém tenta instanciá-la. Definir class Incompleta(Base): pass sem implementar o método abstrato não produz erro nenhum: a classe é criada normalmente e o módulo importa sem reclamação. Só na primeira chamada a Incompleta() vem o TypeError: Can't instantiate abstract class Incompleta without an implementation for abstract method 'salvar'. O mecanismo explica o porquê: o ABCMeta acumula, num conjunto chamado __abstractmethods__, os nomes ainda não implementados, e é o object.__new__ que consulta esse conjunto na hora de construir o objeto. Isso também é o que torna possível uma hierarquia intermediária — uma classe abstrata pode herdar de outra e implementar só parte dos métodos, permanecendo abstrata. A consequência prática é que importar um módulo não valida nada, e é por isso que revisão e linter passaram: para o leitor humano a classe parece completa, e a ferramenta estática não costuma perseguir a cadeia de bases abstratas. A prática de teste que pegaria é simples e barata: todo caminho de código precisa ter pelo menos um teste que construa o objeto. Um teste que apenas importa o módulo e verifica que a classe existe não exercita o __new__ e não descobre nada. Em bases com muitas implementações de um mesmo contrato, vale um teste parametrizado que percorra Base.__subclasses__() e instancie cada uma, o que transforma o esquecimento em falha de teste automática no dia em que a subclasse nova for escrita. Ferramentas de verificação de tipos, como o mypy, também acusam esse caso na análise estática, e é um dos argumentos práticos a favor de adotá-las.
Exercício 4
Um projeto define class Nadador: com o método nadar que usa self.nome, sem que a classe declare esse atributo em lugar nenhum. Funciona porque só é usada como base de Triatleta, que define self.nome no construtor. Um colega tenta reaproveitar Nadador sozinho em outro módulo e recebe AttributeError. Avalie o projeto original e diga como tornar o contrato explícito.
Ver resposta
✓ Resposta: O padrão em si é legítimo e tem nome: mixin. Uma classe mixin fornece comportamento para ser combinado com outras e não se destina a existir sozinha — por isso é normal que ela use atributos que não cria. O problema do código original não é o padrão, é que o contrato ficou implícito: nada no nome, na documentação ou na assinatura avisa que Nadador exige um self.nome fornecido por quem a combinar, e a única forma de descobrir é ler o corpo dos métodos ou tropeçar no AttributeError, como o colega tropeçou. Há três formas de explicitar, combináveis entre si. A primeira, e mais barata, é a convenção de nome mais uma linha de documentação: chamar a classe de NadadorMixin e escrever no docstring que ela requer nome comunica a intenção a quem vai reaproveitar, que é exatamente quem precisa da informação. A segunda é declarar a dependência de forma verificável por ferramenta, anotando nome: str no corpo da classe — sem atribuir valor, o que cria apenas uma anotação em __annotations__ e permite ao mypy acusar a combinação que não fornece o atributo. A terceira, quando o contrato é maior que um atributo, é declarar um Protocol do módulo typing descrevendo o que o mixin espera, e anotar com ele. Vale notar a alternativa de projeto que às vezes é melhor que todas: se nome é comum a todos os papéis, ele pertence a uma base compartilhada, e o que parecia herança múltipla de três mixins vira uma base com nome mais os comportamentos. Herança múltipla resolve bem o caso de somar capacidades ortogonais; quando as partes compartilham estado, costuma ser sinal de que a modelagem quer outra forma — muitas vezes composição, com o objeto guardando os papéis em vez de herdá-los.
Exercício 5
As três formas geométricas deste artigo — Circulo, Retangulo e Triangulo — não herdam de nenhuma classe comum e ainda assim são percorridas pelo mesmo laço. Explique por que isso funciona, e discuta quando vale a pena introduzir uma classe base abstrata que elas passem a herdar, e quando isso é apenas cerimônia.
Ver resposta
✓ Resposta: Funciona porque o Python resolve chamadas de método em tempo de execução, procurando o nome no objeto que recebeu a chamada, sem consultar declaração de tipo nenhuma. O laço pede forma.area(), e cada objeto responde com o seu — não há verificação prévia de que os três pertençam a um mesmo tipo, e não é preciso que pertençam. É o duck typing: o que define a compatibilidade é o conjunto de métodos que o objeto responde, não a linhagem de onde ele veio. Essa é a diferença mais marcante em relação a linguagens de tipagem nominal, em que o polimorfismo exige uma interface declarada e implementada explicitamente. Sobre quando introduzir a base abstrata, a resposta depende de quem escreve as subclasses e de quando o erro precisa aparecer. Vale a pena quando as implementações são muitas, escritas por pessoas diferentes ou ao longo do tempo: o ABC com @abstractmethod transforma o esquecimento de um método em TypeError na instanciação, com mensagem que nomeia o método faltante, em vez de um AttributeError mais adiante, em outro arquivo, quando aquele caminho finalmente for exercitado. Vale também quando há implementação a compartilhar de fato — no sistema de pagamentos deste artigo, o método que monta o recibo é o mesmo para todos os meios, e a base existe por isso, não pela formalidade do contrato. É cerimônia, por outro lado, quando as classes são poucas, moram no mesmo arquivo, foram escritas juntas e não compartilham código nenhum: aí a base abstrata acrescenta um arquivo, uma importação e um nível de indireção sem tornar impossível nenhum erro que já não fosse improvável. Há ainda uma terceira via que costuma ser a melhor dos dois mundos em código moderno: declarar um Protocol do módulo typing, que descreve os métodos exigidos e permite ao verificador de tipos conferir a compatibilidade estaticamente, sem obrigar as classes a herdarem de nada — o contrato fica explícito e verificável, e o acoplamento não aumenta.