Concatenate en Python: cambia parámetros

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
Detailed view of programming code in a dark theme on a computer screen.

ParamSpec conserva la firma completa de una callable, pero algunos decoradores cambian intencionalmente los parámetros visibles. Pueden inyectar un contexto, ocultar una conexión, añadir un lock o adaptar un handler para un framework. typing.Concatenate describe una transformación al inicio de la lista de parámetros.

Esta guía muestra cómo combinar Concatenate con ParamSpec y TypeVar, crear decoradores de inyección, autenticación y sincronización, trabajar con wrappers async y callbacks, y entender los límites de la herramienta.

El problema de inyectar contexto

from collections.abc import Callable

class Contexto:
    usuario: str

def inyectar_contexto(funcion: Callable):
    def wrapper(*args, **kwargs):
        contexto = Contexto()
        contexto.usuario = "sistema"
        return funcion(contexto, *args, **kwargs)
    return wrapper

La función original requiere Contexto primero, mientras la función decorada debe ocultar ese argumento. Sin tipado preciso se pierden autocompletado y validación.

Concatenate con ParamSpec

from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def inyectar_contexto(
    funcion: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        contexto = Contexto()
        contexto.usuario = "sistema"
        return funcion(contexto, *args, **kwargs)
    return wrapper

Concatenate[Contexto, P] significa que la callable original recibe Contexto y después todos los parámetros capturados por P. El wrapper público expone solo P.

Por qué aparece dentro de Callable

Concatenate está diseñado para listas de parámetros de callables. Normalmente aparece como primer argumento de Callable y termina con un ParamSpec.

Callable[Concatenate[Contexto, P], R]

Los tipos explícitos aparecen antes de P. No sirve para insertar parámetros en el medio o al final de la firma capturada.

Inyectar una conexión

class Conexion:
    def ejecutar(self, sql: str) -> list[tuple[object, ...]]:
        ...

def con_conexion(
    funcion: Callable[Concatenate[Conexion, P], R],
) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        conexion = abrir_conexion()
        try:
            return funcion(conexion, *args, **kwargs)
        finally:
            conexion.cerrar()
    return wrapper

El consumidor no proporciona la conexión. El decorador controla creación y cierre, mientras la función de negocio recibe una dependencia tipada explícitamente.

Conservar metadatos

from functools import wraps

@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
    ...

wraps conserva nombre, documentación y referencia a la función original en runtime. Concatenate, ParamSpec y TypeVar preservan la relación estática.

Inyectar un lock

from threading import Lock


def con_lock(
    funcion: Callable[Concatenate[Lock, P], R],
) -> Callable[P, R]:
    lock = Lock()

    @wraps(funcion)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        with lock:
            return funcion(lock, *args, **kwargs)
    return wrapper

El lock se comparte entre llamadas. Documenta granularidad y reentrancia porque la anotación no describe esos detalles.

Inyectar usuario autenticado

class Usuario:
    id: int
    nombre: str

def requiere_usuario(
    funcion: Callable[Concatenate[Usuario, P], R],
) -> Callable[P, R]:
    @wraps(funcion)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        usuario = usuario_actual()
        if usuario is None:
            raise PermissionError("autenticación necesaria")
        return funcion(usuario, *args, **kwargs)
    return wrapper

La función de negocio declara que necesita Usuario, mientras la entrada pública obtiene esa dependencia del entorno.

Varios parámetros inyectados

def inyectar_servicios(
    funcion: Callable[
        Concatenate[Usuario, Conexion, P],
        R,
    ],
) -> Callable[P, R]:
    @wraps(funcion)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        usuario = obtener_usuario()
        conexion = abrir_conexion()
        try:
            return funcion(usuario, conexion, *args, **kwargs)
        finally:
            conexion.cerrar()
    return wrapper

Pueden aparecer varios tipos antes de P. Su orden debe coincidir exactamente con la llamada real.

Transformación inversa

Un wrapper también puede añadir un parámetro para el consumidor y ocultarlo de la función interna.

def exigir_token(
    funcion: Callable[P, R],
) -> Callable[Concatenate[str, P], R]:
    @wraps(funcion)
    def wrapper(
        token: str,
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        validar_token(token)
        return funcion(*args, **kwargs)
    return wrapper

El consumidor ahora proporciona un token antes de los parámetros originales.

Wrappers asíncronos

from collections.abc import Awaitable

class ContextoAsync:
    trace_id: str

def con_trace(
    funcion: Callable[
        Concatenate[ContextoAsync, P],
        Awaitable[R],
    ],
) -> Callable[P, Awaitable[R]]:
    @wraps(funcion)
    async def wrapper(
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        contexto = ContextoAsync()
        contexto.trace_id = crear_trace_id()
        return await funcion(contexto, *args, **kwargs)
    return wrapper

La relación de parámetros es igual a la versión síncrona y el resultado sigue siendo awaitable.

Concatenate en callbacks

def registrar_handler(
    handler: Callable[Concatenate[Evento, P], None],
    *args: P.args,
    **kwargs: P.kwargs,
) -> None:
    evento = esperar_evento()
    handler(evento, *args, **kwargs)

La función de registro controla Evento y el consumidor proporciona los argumentos restantes compatibles con P.

Métodos y self

En métodos, self o cls ya pertenece a la firma capturada. Un decorador que solo reenvía argumentos suele necesitar únicamente ParamSpec. Concatenate es relevante cuando cambia el inicio visible.

Los descriptores vinculan automáticamente el receptor, por lo que inyectar una dependencia después de self puede ser difícil de expresar con un prefijo genérico. Un Protocol específico o overload explícito puede ser más claro.

Límite: solo prefijos

Concatenate añade tipos antes de P. No inserta un parámetro después del primer elemento capturado ni representa un nuevo keyword-only al final. Transformaciones complejas pueden requerir overloads, Protocols o plugins.

Parámetros nombrados

Los tipos añadidos funcionan como parámetros posicionales iniciales. Nombres, defaults y naturaleza keyword-only pueden no expresarse como deseas. Si los nombres públicos son importantes, define un Protocol con una firma explícita de __call__.

Concatenate frente a overload

Concatenate modela una transformación genérica. Overload modela un conjunto finito de formas alternativas. Usa Concatenate cuando cualquier P se conserva tras añadir o quitar un prefijo; usa overload cuando existen pocas firmas concretas con resultados distintos.

Concatenate frente a Protocol

Protocol es más detallado para callables con nombres específicos, atributos extra o métodos. Concatenate es compacto para decoradores genéricos. Ambos pueden combinarse.

Concatenate frente a functools.partial

functools.partial fija argumentos en runtime. Concatenate describe estáticamente la relación entre callables. Un helper tipado para partials puede usar ParamSpec y Concatenate, aunque la inferencia de keywords complejos puede variar.

Cambiar el retorno

def con_contexto_opcional(
    funcion: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R | None]:
    @wraps(funcion)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
        contexto = intentar_contexto()
        if contexto is None:
            return None
        return funcion(contexto, *args, **kwargs)
    return wrapper

Concatenate gestiona parámetros; la anotación de retorno debe mostrar cualquier transformación.

Errores comunes

  • Usar Concatenate sin ParamSpec final: la construcción queda incompleta.
  • Intentar insertar en el medio: solo modela prefijos.
  • Cambiar el orden: los tipos deben coincidir con la llamada real.
  • Olvidar P.args y P.kwargs: no se conservan los parámetros restantes.
  • Ignorar binding de métodos: los descriptores cambian el comportamiento.
  • Suponer que el tipado valida runtime: el wrapper debe construir dependencias correctas.

Ejemplo completo: unidad de trabajo

from collections.abc import Callable
from functools import wraps
from typing import Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

class UnidadTrabajo:
    def __enter__(self):
        return self

    def confirmar(self) -> None:
        ...

    def __exit__(self, exc_type, exc, tb) -> None:
        if exc is not None:
            self.revertir()

    def revertir(self) -> None:
        ...

def transaccional(
    funcion: Callable[Concatenate[UnidadTrabajo, P], R],
) -> Callable[P, R]:
    @wraps(funcion)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        with UnidadTrabajo() as uow:
            resultado = funcion(uow, *args, **kwargs)
            uow.confirmar()
            return resultado
    return wrapper

@transaccional
def crear_pedido(
    uow: UnidadTrabajo,
    cliente_id: int,
    items: list[int],
) -> int:
    ...

El consumidor ve únicamente cliente_id e items. La función interna declara su dependencia explícitamente y sigue siendo fácil de probar.

Probar la firma

Usa reveal_type(), fixtures de mypy o pyright y llamadas inválidas. Las pruebas de runtime también deben verificar orden, binding y ciclo de vida.

Conclusión

typing.Concatenate complementa ParamSpec al modelar callables que añaden o eliminan parámetros al comienzo de una firma. Es útil para contexto, autenticación, locks, conexiones y adaptadores.

La documentación oficial de Concatenate en Python define la construcción. Consulta también ParamSpec en Python y usa Protocol u overloads cuando la transformación no pueda expresarse como prefijo genérico.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Vivid close-up of code on a computer screen showcasing programming details.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Never en Python: marca código inalcanzable

    Aprende typing.Never en Python para funciones sin retorno, código inalcanzable y exhaustividad con assert_never, Literal y Enum.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec en Python: conserva firmas

    Aprende ParamSpec en Python para conservar firmas completas en decoradores, callbacks, wrappers async y funciones de orden superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up of a python snake curled up, showcasing its detailed scales.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs en Python: refina ambas ramas

    Aprende TypeIs en Python para refinar las ramas verdadera y falsa, compararlo con TypeGuard y crear predicados de tipo seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard en Python: refina tipos seguros

    Aprende TypeGuard en Python para refinar tipos y validar colecciones, TypedDict, Protocol y datos externos con comprobaciones reales.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    typing.Self en Python: retornos fluidos

    Aprende typing.Self en Python para métodos fluidos, classmethods, builders, clones, Protocol, context managers y retornos que conservan subclases.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup en Python: múltiples errores

    Aprende ExceptionGroup en Python para múltiples errores, except*, grupos anidados, TaskGroup, filtros, logging y validación por lotes.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026