singledispatch no Python: polimorfismo simples

Publicado em: 25/07/2026
Tempo de leitura: 6 minutos
Código Python com funções especializadas por tipo

Em projetos Python, é comum começar com uma função pequena e depois acrescentar vários blocos if para tratar números, textos, listas, dicionários e objetos próprios. No início, essa solução parece direta. Com o tempo, porém, a função cresce, mistura responsabilidades e se torna difícil de testar. O decorador singledispatch no Python oferece uma alternativa organizada: você mantém uma função pública e registra implementações diferentes conforme o tipo do primeiro argumento.

Neste guia, você aprenderá como usar functools.singledispatch, criar uma implementação padrão, registrar tipos com decoradores, trabalhar com classes, coleções e uniões de tipos, testar o despacho e reconhecer situações em que outra solução é mais adequada. O recurso combina especialmente bem com os conceitos de funções em Python, decoradores, programação orientada a objetos e recursos avançados de classes.

O que é singledispatch?

singledispatch é um decorador da biblioteca padrão que transforma uma função em uma função genérica. Essa função escolhe qual implementação executar com base no tipo do primeiro argumento recebido. O nome “single dispatch” indica justamente que apenas um argumento participa da decisão.

O recurso foi introduzido a partir da proposta descrita na PEP 443. A documentação oficial de functools.singledispatch apresenta a API, o registro de tipos e o comportamento de resolução pela hierarquia de classes.

Primeiro exemplo

Imagine uma função que precisa transformar valores em uma descrição legível. Sem singledispatch, você poderia escrever vários testes com isinstance. Com o decorador, cada caso fica separado:

from functools import singledispatch

@singledispatch
def descrever(valor):
    return f"Valor do tipo {type(valor).__name__}: {valor!r}"

@descrever.register
def _(valor: int):
    return f"Número inteiro: {valor}"

@descrever.register
def _(valor: str):
    return f"Texto com {len(valor)} caracteres"

@descrever.register
def _(valor: list):
    return f"Lista com {len(valor)} itens"

print(descrever(10))
print(descrever("Python"))
print(descrever([1, 2, 3]))
print(descrever({"ativo": True}))

A primeira função é a implementação padrão. Ela será usada quando nenhum tipo mais específico estiver registrado. As demais funções são associadas ao nome público descrever por meio de @descrever.register.

Por que as funções registradas usam o nome _?

É comum nomear as implementações registradas como _ porque o nome individual raramente será chamado diretamente. O que importa é o registro feito na função genérica. Ainda assim, você pode usar nomes descritivos se isso melhorar a leitura:

@descrever.register
def descrever_float(valor: float):
    return f"Número decimal: {valor:.2f}"

O decorador devolve a função original registrada, o que também facilita testes unitários isolados. Portanto, usar um nome claro pode ser útil em módulos maiores.

Registro explícito de tipos

Quando não quiser depender da anotação de tipo, informe o tipo diretamente ao método register:

@descrever.register(tuple)
def _(valor):
    return f"Tupla com {len(valor)} posições"

Essa sintaxe é útil em código legado, em funções cuja anotação não é conveniente ou ao registrar classes importadas de outra biblioteca.

Como o despacho escolhe a implementação?

O Python verifica o tipo real do primeiro argumento e procura a implementação mais específica disponível. Se não houver uma correspondência exata, ele percorre a hierarquia de herança. Assim, uma implementação registrada para uma classe base pode atender suas subclasses.

from collections.abc import Mapping

@descrever.register
def _(valor: Mapping):
    return f"Mapeamento com chaves: {list(valor)[:3]}"

print(descrever({"nome": "Ana", "idade": 30}))

Como dict implementa Mapping, essa versão será usada. Registrar abstrações de collections.abc pode tornar o código mais flexível do que registrar somente classes concretas.

Usando classes próprias

O recurso funciona naturalmente com classes definidas no projeto:

from dataclasses import dataclass

@dataclass
class Produto:
    nome: str
    preco: float

@descrever.register
def _(valor: Produto):
    return f"Produto {valor.nome}: R$ {valor.preco:.2f}"

produto = Produto("Teclado", 199.90)
print(descrever(produto))

Esse padrão é interessante quando diferentes partes do sistema precisam converter objetos para texto, eventos, comandos, documentos ou formatos externos sem concentrar todas as regras em uma única função gigante.

Uniões de tipos

Em versões modernas do Python, uma implementação pode ser registrada para uma união de tipos usando anotações:

@descrever.register
def _(valor: int | float):
    return f"Número: {valor}"

Essa implementação atende inteiros e números de ponto flutuante. Porém, evite uniões enormes. Quando tipos exigem regras diferentes, implementações separadas tendem a ser mais claras.

Despacho considera apenas o primeiro argumento

Esse detalhe é essencial. Em uma função como converter(valor, formato), o despacho considera somente valor. O segundo argumento pode influenciar a lógica interna, mas não seleciona automaticamente outra implementação.

@singledispatch
def converter(valor, formato="texto"):
    raise TypeError(f"Tipo não suportado: {type(valor).__name__}")

@converter.register
def _(valor: int, formato="texto"):
    if formato == "hex":
        return hex(valor)
    return str(valor)

Se a decisão depende de dois ou mais tipos ao mesmo tempo, considere um padrão diferente, como classes com métodos, um dicionário de estratégias ou uma biblioteca de múltiplo despacho.

Inspecionando o registro

A função genérica expõe ferramentas úteis para depuração e testes. O método dispatch informa qual implementação seria escolhida para um tipo:

implementacao = descrever.dispatch(int)
print(implementacao(25))

O atributo registry mostra o mapeamento de tipos registrados. Ele é somente leitura e pode ajudar em diagnósticos:

for tipo, funcao in descrever.registry.items():
    print(tipo, funcao)

Testando funções com singledispatch

Os testes devem cobrir a implementação padrão, cada tipo importante e subclasses relevantes:

def test_descrever_inteiro():
    assert descrever(5) == "Número: 5"

def test_descrever_produto():
    produto = Produto("Mouse", 80.0)
    assert "Mouse" in descrever(produto)

def test_fallback():
    resultado = descrever(object())
    assert "object" in resultado

Também vale testar o resultado de dispatch quando a hierarquia é importante. Isso evita regressões caso outro registro mais específico seja adicionado no futuro.

singledispatchmethod em classes

Quando o comportamento pertence a uma classe, use functools.singledispatchmethod. Ele aplica a mesma ideia a métodos:

from functools import singledispatchmethod

class Exportador:
    @singledispatchmethod
    def exportar(self, valor):
        raise TypeError("Tipo não suportado")

    @exportar.register
    def _(self, valor: str):
        return {"texto": valor}

    @exportar.register
    def _(self, valor: int):
        return {"numero": valor}

exportador = Exportador()
print(exportador.exportar("Academify"))

O despacho usa o primeiro argumento que não seja self ou cls. Esse recurso é útil para serviços de serialização, validadores e adaptadores organizados como objetos.

Quando singledispatch é uma boa escolha?

  • Uma operação possui comportamentos claramente diferentes conforme o tipo.
  • Você quer manter uma API pública única.
  • Novos tipos podem ser adicionados sem alterar a implementação principal.
  • A hierarquia de classes já representa bem as relações entre os dados.
  • As implementações podem ser testadas separadamente.

Quando evitar?

Não use o recurso apenas para eliminar qualquer if. Condições simples continuam sendo mais fáceis de entender em muitos casos. Evite singledispatch quando a decisão depende do valor, e não do tipo; quando vários argumentos definem o comportamento; quando a função possui poucos casos estáveis; ou quando métodos polimórficos nas próprias classes expressam melhor a responsabilidade.

Também não transforme a função genérica em um registro global descontrolado. Se módulos distantes registrarem implementações durante a importação, pode ficar difícil descobrir de onde veio determinado comportamento. Mantenha os registros próximos da função ou em módulos de extensão bem documentados.

Erros comuns

  • Registrar o tipo errado: confira a anotação do primeiro argumento.
  • Esperar despacho pelo segundo parâmetro: somente o primeiro participa.
  • Usar tipos muito concretos: abstrações como Mapping ou Sequence podem ser mais apropriadas.
  • Esconder o fallback: a implementação padrão deve produzir uma resposta segura ou lançar um erro claro.
  • Confundir type hints com validação: a anotação é usada no registro, mas não valida automaticamente o conteúdo do objeto.

Conclusão

O singledispatch no Python permite organizar uma operação em implementações especializadas pelo tipo do primeiro argumento. Ele reduz cadeias extensas de isinstance, preserva uma interface única e aproveita a hierarquia de classes para escolher o comportamento mais específico.

Use o recurso quando o domínio realmente possui variações por tipo e mantenha uma implementação padrão explícita. Combine registros pequenos, testes, abstrações adequadas e documentação. Assim, a função genérica permanece extensível sem se transformar em uma fonte invisível de complexidade.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código Python com descriptors e atributos
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Descriptors em Python: guia prático

    Aprenda descriptors em Python com __get__, __set__, validação, property, armazenamento por instância, testes e boas práticas.

    Ler mais

    Tempo de leitura: 6 minutos
    22/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como criar um instalador .exe com ícone personalizado no Python

    Se você já desenvolveu algum script útil, provavelmente já se perguntou como criar um instalador .exe com ícone personalizado no

    Ler mais

    Tempo de leitura: 11 minutos
    25/04/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar herança múltipla no Python sem bugar seu código

    Entender como usar herança múltipla no Python sem bugar seu código é um dos grandes marcos na jornada de qualquer

    Ler mais

    Tempo de leitura: 9 minutos
    21/04/2026
    Uso do super em Python para resolver problemas de herança
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar super() no Python e resolver erros de herança

    Entender como usar super() no Python é um divisor de águas para qualquer desenvolvedor que deseja dominar a Programação Orientada

    Ler mais

    Tempo de leitura: 9 minutos
    11/04/2026
    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como ler arquivos gigantes sem travar o Python

    Lidar com grandes volumes de dados é um desafio comum na rotina de quem trabalha com programação e ciência de

    Ler mais

    Tempo de leitura: 12 minutos
    16/03/2026
    Uso de multiprocessing em Python para acelerar scripts
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar multiprocessing em Python e acelerar seu script

    Você já sentiu que seu computador tem muito mais poder do que o seu código está realmente utilizando? Se você

    Ler mais

    Tempo de leitura: 11 minutos
    15/03/2026