Polimorfismo em Python: O que É e Como Funciona | Python Brasil
Entenda polimorfismo em Python: duck typing, Protocol, @singledispatch, sobrecarga de operadores, ABC e padrões de design. Guia completo com exemplos.
O que é Polimorfismo?
Polimorfismo significa “muitas formas” e é a capacidade de objetos diferentes responderem à mesma mensagem (método) de maneiras distintas. Em Python, o polimorfismo é natural graças ao duck typing: “se anda como um pato e faz quack como um pato, então é um pato”.
Existem três formas principais de polimorfismo em Python: polimorfismo ad-hoc (sobrecarga de operadores), polimorfismo paramétrico (generics/type hints) e polimorfismo de subtipo (herança e Protocols).
Polimorfismo por herança
A forma mais clássica: subclasses sobrescrevem métodos da classe base para fornecer comportamentos específicos.
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self) -> float:
pass
def descricao(self):
return f"{self.__class__.__name__} com área {self.area():.2f}"
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
def area(self):
return 3.14159 * self.raio ** 2
class Retangulo(Forma):
def __init__(self, largura, altura):
self.largura = largura
self.altura = altura
def area(self):
return self.largura * self.altura
class Triangulo(Forma):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return (self.base * self.altura) / 2
# Polimorfismo em ação — mesmo código, comportamentos diferentes
formas = [Circulo(5), Retangulo(4, 6), Triangulo(3, 8)]
for forma in formas:
print(forma.descricao())
# Circulo com área 78.54
# Retangulo com área 24.00
# Triangulo com área 12.00
# Função genérica que funciona com qualquer Forma
def area_total(formas):
return sum(f.area() for f in formas)
print(area_total(formas)) # 114.54
Duck Typing: polimorfismo sem herança
Em Python, você não precisa de uma classe base comum para ter polimorfismo. Basta que os objetos tenham os métodos esperados — isso é chamado de duck typing.
class Arquivo:
def ler(self):
return "Lendo arquivo do disco"
class BancoDeDados:
def ler(self):
return "Lendo do banco de dados"
class API:
def ler(self):
return "Lendo da API externa"
class Cache:
def ler(self):
return "Lendo do cache em memória"
# Função polimórfica — funciona com qualquer objeto que tenha 'ler'
def processar_dados(fonte):
dados = fonte.ler()
print(f"Processando: {dados}")
for fonte in [Arquivo(), BancoDeDados(), API(), Cache()]:
processar_dados(fonte)
Protocol: subtyping estrutural (PEP 544)
O Python 3.8 introduziu Protocol do módulo typing, que formaliza o duck typing com suporte a verificação estática de tipos. Um Protocol define uma interface estrutural — qualquer classe que implemente os métodos necessários é compatível, sem precisar herdar explicitamente.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Legivel(Protocol):
def ler(self) -> str: ...
class Gravavel(Protocol):
def gravar(self, dados: str) -> None: ...
class FonteDeDados(Legivel, Gravavel, Protocol):
pass
class ArquivoCSV:
def ler(self) -> str:
return "dados,do,csv"
def gravar(self, dados: str) -> None:
print(f"Gravando CSV: {dados}")
class BancoPostgres:
def ler(self) -> str:
return "SELECT * FROM tabela"
def gravar(self, dados: str) -> None:
print(f"INSERT INTO tabela: {dados}")
def sincronizar(origem: Legivel, destino: Gravavel) -> None:
dados = origem.ler()
destino.gravar(dados)
# Funciona sem nenhuma herança explícita
sincronizar(ArquivoCSV(), BancoPostgres())
# Com @runtime_checkable, isinstance funciona
print(isinstance(ArquivoCSV(), Legivel)) # True
print(isinstance(BancoPostgres(), Legivel)) # True
A diferença entre ABC e Protocol: use ABC quando quiser herança explícita e métodos abstratos obrigatórios verificados em tempo de execução. Use Protocol quando quiser compatibilidade estrutural verificada pelo type checker sem forçar herança.
Polimorfismo ad-hoc: sobrecarga de operadores
Python permite que suas classes definam o comportamento dos operadores matemáticos, de comparação e outros via dunder methods:
from functools import total_ordering
@total_ordering # Gera __le__, __gt__, __ge__ a partir de __eq__ e __lt__
class Dinheiro:
def __init__(self, valor, moeda="BRL"):
self.valor = valor
self.moeda = moeda
def __repr__(self):
return f"Dinheiro({self.valor:.2f}, '{self.moeda}')"
def __str__(self):
simbolos = {"BRL": "R$", "USD": "$", "EUR": "€"}
simbolo = simbolos.get(self.moeda, self.moeda)
return f"{simbolo} {self.valor:.2f}"
def _verificar_moeda(self, outro):
if self.moeda != outro.moeda:
raise ValueError(f"Moedas incompatíveis: {self.moeda} e {outro.moeda}")
def __add__(self, outro):
self._verificar_moeda(outro)
return Dinheiro(self.valor + outro.valor, self.moeda)
def __sub__(self, outro):
self._verificar_moeda(outro)
return Dinheiro(self.valor - outro.valor, self.moeda)
def __mul__(self, fator):
return Dinheiro(self.valor * fator, self.moeda)
def __eq__(self, outro):
return self.valor == outro.valor and self.moeda == outro.moeda
def __lt__(self, outro):
self._verificar_moeda(outro)
return self.valor < outro.valor
def __hash__(self):
return hash((self.valor, self.moeda))
preco = Dinheiro(100.0)
desconto = Dinheiro(15.0)
final = preco - desconto
print(final) # R$ 85.00
print(final * 2) # R$ 170.00
print(preco > desconto) # True
# Funciona em sorted(), min(), max() graças ao @total_ordering
precos = [Dinheiro(50), Dinheiro(30), Dinheiro(80)]
print(min(precos)) # R$ 30.00
@singledispatch: sobrecarga de funções
functools.singledispatch permite criar funções com comportamentos diferentes para cada tipo de argumento — polimorfismo em funções:
from functools import singledispatch
@singledispatch
def processar(dado):
raise TypeError(f"Tipo não suportado: {type(dado)}")
@processar.register(str)
def _(dado):
return f"String processada: {dado.upper()}"
@processar.register(int)
@processar.register(float)
def _(dado):
return f"Número processado: {dado * 2}"
@processar.register(list)
def _(dado):
return f"Lista processada: {len(dado)} elementos"
@processar.register(dict)
def _(dado):
return f"Dicionário processado: {list(dado.keys())}"
print(processar("hello")) # String processada: HELLO
print(processar(42)) # Número processado: 84
print(processar([1, 2, 3])) # Lista processada: 3 elementos
print(processar({"a": 1})) # Dicionário processado: ['a']
Padrões de design que usam polimorfismo
Strategy Pattern
from abc import ABC, abstractmethod
class EstrategiaDesconto(ABC):
@abstractmethod
def calcular(self, preco: float) -> float:
pass
class SemDesconto(EstrategiaDesconto):
def calcular(self, preco):
return preco
class DescontoPercentual(EstrategiaDesconto):
def __init__(self, percentual):
self.percentual = percentual
def calcular(self, preco):
return preco * (1 - self.percentual / 100)
class DescontoFixo(EstrategiaDesconto):
def __init__(self, valor):
self.valor = valor
def calcular(self, preco):
return max(0, preco - self.valor)
class Pedido:
def __init__(self, preco, estrategia: EstrategiaDesconto):
self.preco = preco
self.estrategia = estrategia
def total(self):
return self.estrategia.calcular(self.preco)
pedidos = [
Pedido(100, SemDesconto()),
Pedido(100, DescontoPercentual(20)),
Pedido(100, DescontoFixo(15)),
]
for p in pedidos:
print(f"Total: R$ {p.total():.2f}")
# Total: R$ 100.00
# Total: R$ 80.00
# Total: R$ 85.00
Command Pattern
class Comando(Protocol):
def executar(self) -> None: ...
def desfazer(self) -> None: ...
class AdicionarItem:
def __init__(self, lista, item):
self.lista = lista
self.item = item
def executar(self):
self.lista.append(self.item)
def desfazer(self):
self.lista.remove(self.item)
class RemoverItem:
def __init__(self, lista, item):
self.lista = lista
self.item = item
self._backup = None
def executar(self):
if self.item in self.lista:
self._backup = self.item
self.lista.remove(self.item)
def desfazer(self):
if self._backup:
self.lista.append(self._backup)
historico = []
lista = [1, 2, 3]
cmd1 = AdicionarItem(lista, 4)
cmd1.executar()
historico.append(cmd1)
cmd2 = RemoverItem(lista, 2)
cmd2.executar()
historico.append(cmd2)
print(lista) # [1, 3, 4]
# Desfazer todas as operações
for cmd in reversed(historico):
cmd.desfazer()
print(lista) # [1, 2, 3]
ABC vs Protocol: quando usar cada um?
Use ABC quando: a hierarquia de herança é importante semanticamente; você quer garantir que subclasses implementem métodos específicos em tempo de criação; há lógica compartilhada na classe base.
Use Protocol quando: você quer compatibilidade com código que não herda de sua classe; está trabalhando com duck typing e quer type checking estático; quer interfaces menores e mais composáveis.
Erros comuns
Verificar tipos manualmente com isinstance ou type() dentro de funções polimórficas geralmente é um sinal de que o polimorfismo não está sendo usado corretamente. Se você escreve if isinstance(obj, ClasseA): ... elif isinstance(obj, ClasseB): ..., considere mover esse comportamento para métodos das classes.
Por que polimorfismo importa?
O polimorfismo permite escrever código genérico e flexível que funciona com diferentes tipos de objetos sem verificar o tipo de cada um. Isso torna o código mais extensível — você pode adicionar novos tipos sem alterar o código existente, seguindo o Princípio Aberto/Fechado (Open/Closed Principle). O resultado é código mais fácil de testar, manter e evoluir.
Termos Relacionados
- Classe - Base para criação de tipos
- Herança - Uma forma de implementar polimorfismo
- Encapsulamento - Outro pilar da POO