Voltar ao Glossario
Glossario Python

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