Polimorfismo em Python Orientado a Objetos: interfaces de fato e duck typing

Capítulo 7

Tempo estimado de leitura: 9 minutos

+ Exercício

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 diferentes

Duck 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 = dados

Agora 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 resultado

Passo 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 implementa salvar() 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 retorna str, às vezes retorna dict. Defina e mantenha um retorno consistente.

Resumo de “interfaces de fato” úteis (modelos de contrato)

AbstraçãoMétodo esperadoRetornoUso típico
Salvávelsalvar(dados)NonePersistência plugável
Totalizáveltotal()númeroCálculo de totais sem condicionais
Renderizávelrender()strGeração de saída (HTML/texto)

Agora responda o exercício sobre o conteúdo:

Em um código que usa duck typing para aplicar polimorfismo, o que torna um objeto adequado para ser passado a uma função como renderizar_pagina(componente)?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

No duck typing, o foco é na interface de fato: basta o objeto oferecer render() (ou outro método esperado) e cumprir o contrato, como retornar str. Não é necessário herança nem condicionais por tipo.

Próximo capitúlo

Composição em Python Orientado a Objetos: construir sistemas por partes reutilizáveis

Arrow Right Icon
Capa do Ebook gratuito Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas
50%

Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.