O que é polimorfismo (na prática)
Polimorfismo é a capacidade de diferentes objetos responderem à mesma mensagem (chamada de método/atributo) com comportamentos diferentes. Em vez de perguntar “que tipo é esse objeto?”, você pergunta “esse objeto sabe fazer X?”.
Em Python, isso aparece com força por causa do duck typing: se algo “parece um pato” (tem o método esperado), você usa como pato. Ou seja, o foco é na interface de fato (o conjunto de métodos/atributos que seu código usa), não no tipo nominal.
Mensagem única, comportamentos distintos
Se seu código chama salvar(), qualquer objeto que implemente salvar() pode ser usado. Um pode salvar em arquivo, outro em banco, outro em memória, e o chamador não precisa saber qual é qual.
def persistir(obj): # opera sobre uma abstração: "algo que salva" obj.salvar() # mesma mensagem, implementações diferentesDuck typing e “interfaces de fato”
Uma “interface de fato” é o contrato implícito: “para funcionar aqui, o objeto precisa ter estes métodos com esta assinatura e semântica”. Em Python, você pode documentar isso e reforçar com testes e (opcionalmente) tipagem estática.
Exemplo: função que opera sobre abstrações
Vamos criar uma função renderizar_pagina que aceita qualquer objeto com método render() retornando uma string.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
def renderizar_pagina(componente): html = componente.render() # contrato: existe render() e retorna str return f"<main>{html}</main>"Agora, diferentes componentes podem ser usados sem if por tipo.
class Banner: def __init__(self, texto): self.texto = texto def render(self): return f"<section class='banner'>{self.texto}</section>"class Lista: def __init__(self, itens): self.itens = itens def render(self): lis = "".join(f"<li>{i}</li>" for i in self.itens) return f"<ul>{lis}</ul>"print(renderizar_pagina(Banner("Bem-vindo")))print(renderizar_pagina(Lista(["A", "B", "C"])))Quando o duck typing falha: erros mais claros
Se você passar um objeto sem render(), o Python levantará AttributeError. Em código de aplicação, você pode preferir falhar cedo com uma mensagem mais clara.
def renderizar_pagina(componente): if not hasattr(componente, "render"): raise TypeError("componente precisa implementar render()") html = componente.render() if not isinstance(html, str): raise TypeError("render() deve retornar str") return f"<main>{html}</main>"Use esse tipo de verificação com parcimônia: o ideal é que o contrato seja garantido por testes e por uma boa API.
Protocolos informais: documentando o contrato
Um protocolo informal é simplesmente: “para usar esta função, o objeto deve ter métodos X, Y, Z”. Documente isso no docstring e mantenha exemplos de uso.
def calcular_total(itens): """Soma itens que implementam total(). Contrato: cada item deve ter método total() -> número.""" return sum(item.total() for item in itens)Isso permite que você adicione novos tipos de item sem tocar em calcular_total.
Refatoração: substituindo condicionais por polimorfismo
Um sinal clássico de falta de polimorfismo é um bloco com vários if/elif baseado em tipo, string ou “modo”. A seguir, um exemplo típico e como refatorar.
Antes: lógica com condicionais
Imagine um sistema que precisa salvar dados em destinos diferentes. Uma versão inicial pode ficar assim:
def salvar_dados(destino, dados): if destino == "arquivo": with open("saida.txt", "w", encoding="utf-8") as f: f.write(dados) elif destino == "banco": # simulação print(f"INSERT no banco: {dados}") elif destino == "memoria": # simulação global CACHE CACHE = dados else: raise ValueError("destino desconhecido")Problemas comuns: a função cresce a cada novo destino, mistura responsabilidades e fica difícil testar cada caminho.
Depois: polimorfismo com uma API comum (salvar)
Vamos criar classes com a mesma “interface de fato”: salvar(dados).
class SalvamentoEmArquivo: def __init__(self, caminho): self.caminho = caminho def salvar(self, dados): with open(self.caminho, "w", encoding="utf-8") as f: f.write(dados)class SalvamentoEmBanco: def __init__(self, conexao): self.conexao = conexao # aqui seria um objeto real de conexão def salvar(self, dados): # simulação print(f"INSERT no banco via {self.conexao}: {dados}")class SalvamentoEmMemoria: def __init__(self): self.cache = None def salvar(self, dados): self.cache = dadosAgora a função de alto nível não precisa de condicionais:
def salvar_dados(salvador, dados): """Contrato: salvador implementa salvar(dados).""" salvador.salvar(dados)Exemplos de uso:
salvar_dados(SalvamentoEmArquivo("saida.txt"), "olá")salvar_dados(SalvamentoEmBanco("CONN1"), "olá")mem = SalvamentoEmMemoria()salvar_dados(mem, "olá")print(mem.cache) # valida o resultadoPasso a passo da refatoração (checklist)
- Identifique o eixo de variação: o que muda entre os ramos do
if? (a forma de salvar). - Defina a mensagem comum: qual método representa a ação? (ex.:
salvar(dados)). - Crie uma classe por variação: cada uma implementa a mesma mensagem, com sua regra interna.
- Troque condicionais por composição: a função recebe um objeto que sabe salvar.
- Valide com exemplos: rode cenários para cada implementação e confirme o comportamento.
Outro exemplo: calcular_total() sem perguntar o tipo
Vamos supor que você tem itens diferentes (produto físico, serviço, assinatura) e quer somar o total. Evite if item.tipo == .... Faça cada item saber calcular seu total.
class Produto: def __init__(self, nome, preco, quantidade): self.nome = nome self.preco = preco self.quantidade = quantidade def total(self): return self.preco * self.quantidadeclass Servico: def __init__(self, descricao, valor_hora, horas): self.descricao = descricao self.valor_hora = valor_hora self.horas = horas def total(self): return self.valor_hora * self.horasclass Assinatura: def __init__(self, plano, mensalidade, meses): self.plano = plano self.mensalidade = mensalidade self.meses = meses def total(self): return self.mensalidade * self.mesesdef calcular_total(itens): return sum(item.total() for item in itens)itens = [ Produto("Teclado", 100.0, 2), Servico("Instalação", 80.0, 1.5), Assinatura("Pro", 30.0, 6),]print(calcular_total(itens))Repare que calcular_total não muda quando você adiciona um novo tipo de item, desde que ele implemente total().
Protocolos formais (opcional): usando typing.Protocol para deixar explícito
Duck typing funciona em tempo de execução, mas você pode tornar o contrato explícito para ferramentas de análise estática (e para leitores do código) com Protocol. Isso não muda o comportamento do Python em runtime, mas melhora a clareza.
from typing import Protocolclass Salvavel(Protocol): def salvar(self, dados: str) -> None: ...def salvar_dados(salvador: Salvavel, dados: str) -> None: salvador.salvar(dados)Qualquer classe com salvar(self, dados: str) -> None será aceita por type checkers, mesmo sem herdar de nada.
Exercícios de refatoração (com validação por exemplos)
Exercício 1: substituir if/elif por render()
Cenário: você tem uma função que gera saída diferente dependendo do tipo de conteúdo.
def render(conteudo): if conteudo["tipo"] == "texto": return f"<p>{conteudo['valor']}</p>" elif conteudo["tipo"] == "imagem": return f"<img src='{conteudo['src']}' />" elif conteudo["tipo"] == "link": return f"<a href='{conteudo['href']}'>{conteudo['rotulo']}</a>" else: raise ValueError("tipo desconhecido")Tarefa: crie classes Texto, Imagem, Link, cada uma com render(). Depois, reescreva renderizar_pagina para receber uma lista de objetos e concatenar obj.render().
Validação (exemplo esperado):
componentes = [Texto("Oi"), Link("https://ex.com", "site"), Imagem("/a.png")]html = "".join(c.render() for c in componentes)print(html)Exercício 2: estratégia de desconto com polimorfismo
Cenário: descontos diferentes por regra.
def aplicar_desconto(tipo, subtotal): if tipo == "nenhum": return subtotal if tipo == "percentual": return subtotal * 0.9 if tipo == "fixo": return max(0, subtotal - 50) raise ValueError("tipo inválido")Tarefa: crie classes com método aplicar(subtotal): SemDesconto, DescontoPercentual, DescontoFixo. Reescreva para:
def total_com_desconto(desconto, subtotal): return desconto.aplicar(subtotal)Validação (exemplo esperado):
print(total_com_desconto(SemDesconto(), 200))print(total_com_desconto(DescontoPercentual(0.10), 200))print(total_com_desconto(DescontoFixo(50), 40))Exercício 3: persistência plugável (salvar) com teste simples
Cenário: você quer testar a lógica sem escrever em arquivo nem acessar banco.
Tarefa: implemente SalvamentoEmMemoria (ou um “fake”) e use-o para validar que sua função chama salvar() corretamente.
class SalvamentoFake: def __init__(self): self.chamadas = [] def salvar(self, dados): self.chamadas.append(dados)def processar_e_salvar(salvador, texto): # simulação de processamento saida = texto.strip().upper() salvador.salvar(saida)fake = SalvamentoFake()processar_e_salvar(fake, " oi ")print(fake.chamadas) # esperado: ['OI']Erros comuns ao aplicar polimorfismo
- Interface inconsistente: uma classe implementa
salvar(dados), outra implementasalvar()sem parâmetros. Padronize assinatura e semântica. - Polimorfismo “de mentirinha”: criar classes, mas ainda manter
if isinstance(...)no chamador. Se você precisa checar tipo, o contrato não está bem definido. - Excesso de abstração cedo demais: comece pelo contrato mínimo (um método) e só aumente quando houver necessidade real.
- Retornos incompatíveis: por exemplo,
render()às vezes retornastr, às vezes retornadict. Defina e mantenha um retorno consistente.
Resumo de “interfaces de fato” úteis (modelos de contrato)
| Abstração | Método esperado | Retorno | Uso típico |
|---|---|---|---|
| Salvável | salvar(dados) | None | Persistência plugável |
| Totalizável | total() | número | Cálculo de totais sem condicionais |
| Renderizável | render() | str | Geração de saída (HTML/texto) |