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 resultadoTambé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
MappingouSequencepodem 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.






