ParamSpec preserva a assinatura completa de uma função, mas alguns decoradores não mantêm exatamente os mesmos parâmetros. Eles podem injetar um contexto, remover uma dependência do chamador, adicionar um lock ou adaptar uma callable para um framework. typing.Concatenate descreve essa transformação no início da lista de parâmetros.
Neste guia, você aprenderá a combinar Concatenate com ParamSpec e TypeVar, criar decoradores de injeção, autenticação e sincronização, lidar com métodos, callbacks e wrappers assíncronos, além de reconhecer os limites da ferramenta.
O problema de um decorador que injeta contexto
from collections.abc import Callable
class Contexto:
usuario: str
def injetar_contexto(funcao: Callable):
def wrapper(*args, **kwargs):
contexto = Contexto()
contexto.usuario = "sistema"
return funcao(contexto, *args, **kwargs)
return wrapperA função original exige um Contexto como primeiro parâmetro, mas a função decorada não deve expor esse argumento. Sem uma anotação precisa, autocomplete e validação de chamadas são perdidos.
Concatenate com ParamSpec
from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def injetar_contexto(
funcao: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
contexto = Contexto()
contexto.usuario = "sistema"
return funcao(contexto, *args, **kwargs)
return wrapperConcatenate[Contexto, P] significa que a callable original recebe primeiro um Contexto e, em seguida, todos os parâmetros capturados por P. O wrapper público expõe apenas P.
Por que Concatenate aparece dentro de Callable
Concatenate é uma construção voltada à lista de parâmetros de callables. Ele normalmente aparece como primeiro argumento de Callable e termina com um ParamSpec.
Callable[Concatenate[Contexto, P], R]Os tipos explícitos vêm antes de P. A ferramenta não serve para inserir parâmetros no meio ou no fim da assinatura capturada.
Injetando uma conexão
class Conexao:
def executar(self, sql: str) -> list[tuple[object, ...]]:
...
def com_conexao(
funcao: Callable[Concatenate[Conexao, P], R],
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
conexao = abrir_conexao()
try:
return funcao(conexao, *args, **kwargs)
finally:
conexao.fechar()
return wrapperO chamador não fornece a conexão. O decorador controla criação e encerramento, enquanto a função interna recebe uma dependência explicitamente tipada.
Preservando metadados
from functools import wraps
def com_conexao(
funcao: Callable[Concatenate[Conexao, P], R],
) -> Callable[P, R]:
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
...
return wrapperwraps preserva nome, docstring e referência à função original em runtime. Concatenate, ParamSpec e TypeVar preservam a relação estática. Use ambos.
Adicionando um lock
from threading import Lock
def com_lock(
funcao: Callable[Concatenate[Lock, P], R],
) -> Callable[P, R]:
lock = Lock()
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
with lock:
return funcao(lock, *args, **kwargs)
return wrapperO lock pode ser compartilhado por todas as chamadas da função decorada. Documente esse comportamento, pois a anotação não descreve granularidade, reentrância ou desempenho.
Injetando usuário autenticado
class Usuario:
id: int
nome: str
def requer_usuario(
funcao: Callable[Concatenate[Usuario, P], R],
) -> Callable[P, R]:
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
usuario = usuario_atual()
if usuario is None:
raise PermissionError("autenticação necessária")
return funcao(usuario, *args, **kwargs)
return wrapperA função de negócio declara explicitamente que depende de Usuario, enquanto o ponto de entrada público obtém essa informação do ambiente.
Mais de um parâmetro injetado
def injetar_servicos(
funcao: Callable[
Concatenate[Usuario, Conexao, P],
R,
],
) -> Callable[P, R]:
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
usuario = obter_usuario()
conexao = abrir_conexao()
try:
return funcao(usuario, conexao, *args, **kwargs)
finally:
conexao.fechar()
return wrapperVários tipos podem aparecer antes de P. A ordem deve corresponder exatamente à função original.
Transformação inversa
Um wrapper também pode adicionar um parâmetro para o chamador e ocultá-lo da função interna. Nesse caso, a callable recebida usa P e a callable retornada usa Concatenate.
def exigir_token(
funcao: Callable[P, R],
) -> Callable[Concatenate[str, P], R]:
@wraps(funcao)
def wrapper(
token: str,
*args: P.args,
**kwargs: P.kwargs,
) -> R:
validar_token(token)
return funcao(*args, **kwargs)
return wrapperAgora o chamador precisa fornecer um token antes dos parâmetros originais.
Wrappers assíncronos
from collections.abc import Awaitable
class ContextoAsync:
trace_id: str
def com_trace(
funcao: Callable[
Concatenate[ContextoAsync, P],
Awaitable[R],
],
) -> Callable[P, Awaitable[R]]:
@wraps(funcao)
async def wrapper(
*args: P.args,
**kwargs: P.kwargs,
) -> R:
contexto = ContextoAsync()
contexto.trace_id = criar_trace_id()
return await funcao(contexto, *args, **kwargs)
return wrapperA relação dos parâmetros é igual à versão síncrona; o retorno é um Awaitable.
Concatenate em callbacks
def registrar_handler(
handler: Callable[Concatenate[Evento, P], None],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
evento = aguardar_evento()
handler(evento, *args, **kwargs)A função de registro controla o Evento e o consumidor fornece os demais argumentos compatíveis com P.
Métodos e self
Em métodos, self ou cls já faz parte da assinatura. Um decorador genérico que apenas encaminha parâmetros normalmente precisa somente de ParamSpec. Concatenate é necessário quando você altera explicitamente o início visível da assinatura.
class Servico:
@requer_usuario
def executar(
usuario: Usuario,
self,
tarefa: str,
) -> None:
...Esse formato é pouco natural porque o descritor de método espera self primeiro. Ao decorar métodos, planeje cuidadosamente a ordem e teste o comportamento do binding. Muitas vezes é melhor injetar a dependência depois de self com um decorador específico para métodos, algo que Concatenate não modela diretamente de forma simples.
Limite: apenas o início da assinatura
Concatenate adiciona tipos antes de P. Ele não insere um parâmetro depois do primeiro item capturado, nem representa facilmente a adição de um keyword-only no final. Para transformações mais complexas, overloads explícitos, Protocols específicos ou plugins de analisador podem ser necessários.
Parâmetros nomeados
Os tipos adicionados por Concatenate são tratados como parâmetros posicionais na relação da callable. Nome, default e natureza keyword-only podem não ser expressos da forma desejada. Se a API pública depende fortemente de nomes, considere declarar um Protocol com __call__ explícito.
Concatenate versus overload
Concatenate descreve uma transformação genérica de assinatura. Overload descreve um conjunto finito de assinaturas alternativas. Use Concatenate quando qualquer P é preservado após adicionar ou remover parâmetros iniciais. Use overload quando existem alguns formatos concretos com retornos diferentes.
Concatenate versus Protocol
Protocol é mais detalhado para callables com parâmetros nomeados específicos, atributos adicionais ou métodos. Concatenate é compacto para decoradores genéricos. As duas ferramentas podem ser combinadas.
Concatenate versus functools.partial
functools.partial fixa argumentos em runtime. Concatenate descreve estaticamente a mudança entre callables. Um helper tipado que cria partials pode usar ParamSpec e Concatenate, mas a inferência de argumentos nomeados complexos pode variar entre verificadores.
Retorno transformado
def com_contexto_opcional(
funcao: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R | None]:
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
contexto = tentar_contexto()
if contexto is None:
return None
return funcao(contexto, *args, **kwargs)
return wrapperConcatenate cuida dos parâmetros; TypeVar e a anotação do retorno devem mostrar qualquer mudança no resultado.
Erros comuns
- Usar Concatenate sem ParamSpec no final: a construção fica incompleta.
- Inserir parâmetros no meio: Concatenate modela apenas prefixos.
- Trocar a ordem: os tipos devem acompanhar a chamada real.
- Esquecer P.args e P.kwargs: o wrapper não preserva os parâmetros restantes.
- Ignorar binding de métodos: self e descritores podem mudar o comportamento.
- Presumir que a tipagem valida runtime: o wrapper ainda precisa construir e encaminhar objetos corretos.
Exemplo completo: unidade de trabalho
from collections.abc import Callable
from functools import wraps
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
class UnidadeTrabalho:
def __enter__(self):
return self
def confirmar(self) -> None:
...
def __exit__(self, exc_type, exc, tb) -> None:
if exc is not None:
self.reverter()
def reverter(self) -> None:
...
def transacional(
funcao: Callable[Concatenate[UnidadeTrabalho, P], R],
) -> Callable[P, R]:
@wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
with UnidadeTrabalho() as uow:
resultado = funcao(uow, *args, **kwargs)
uow.confirmar()
return resultado
return wrapper
@transacional
def criar_pedido(
uow: UnidadeTrabalho,
cliente_id: int,
itens: list[int],
) -> int:
...Para o chamador, criar_pedido() recebe apenas cliente_id e itens. A função interna continua declarando sua dependência de forma explícita e testável.
Testando a assinatura
Use reveal_type(), testes de mypy ou pyright e chamadas intencionalmente inválidas. Teste também runtime para verificar ordem, binding e ciclo de vida das dependências.
Conclusão
typing.Concatenate complementa ParamSpec ao modelar callables que adicionam ou removem parâmetros no começo da assinatura. Ele é valioso para injeção de contexto, autenticação, locks, conexões e adaptadores.
A documentação oficial de Concatenate no Python detalha a construção. Consulte também o guia de ParamSpec no Python e use Protocol ou overloads quando a transformação não puder ser representada como um prefixo genérico.







