如何用Concatenate和ParamSpec注解处理仅关键字参数的装饰器?
问题:如何用PEP 612注解处理仅关键字参数的装饰器
我对PEP 612的理解是否正确?它支持注解移除被装饰函数第一个参数的装饰器,但目前无法完整注解作用于仅关键字(KW-only)参数的装饰器?
我想要实现这样的装饰器:
def call_authenticated( func: Callable[Concatenate[AuthenticatedClient, P], Awaitable[R]], client: AuthenticatedClient, ) -> Callable[P, Awaitable[R]]: async def wrapped(*a, **k): return await func(*a, **k, client=client) return wrapped
被装饰的函数签名如下:
async def some_func(args1: str, arg2: int, *, client: AuthenticatedClient) -> Something: ...
但这会触发mypy报错:Argument "client" has incompatible type "AuthenticatedClient"; expected "P.kwargs"
以下是最小非工作示例(MNWE):
from typing import Callable, TypeVar, ParamSpec, Awaitable, Concatenate P = ParamSpec("P") T = TypeVar("T") async def to_be_wrapped(x: int, y: str, *, z: dict) -> str: return str(x) + str(y) + str(z) def force_z( func: Callable[Concatenate[dict, P], Awaitable[T]] ) -> Callable[P, Awaitable[T]]: async def wrapped(*args: P.args, **kwargs: P.kwargs): # 错误:参数1的类型不兼容 "*P.args";预期类型为 "dict[Any, Any]" [arg-type] # 错误:参数"z"的类型不兼容 "dict[str, object]";预期类型为 "P.kwargs" [arg-type] return func(*args, **kwargs, z={"foo": "bar"}) return wrapped async def main(): # 错误:传入"force_z"的参数1类型不兼容 # "Callable[[int, str, NamedArg(dict[Any, Any], 'z')], Coroutine[Any, Any, str]]"; # 预期类型为 "Callable[[dict[Any, Any], str, NamedArg(dict[Any, Any], 'z')], Awaitable[str]]" [arg-type] await force_z(to_be_wrapped)(1, "")
解决方案
你的理解是对的:PEP 612的Concatenate仅针对位置参数做拼接或移除,对仅关键字参数的处理确实存在原生局限。要正确注解这类自动注入仅关键字参数的装饰器,需要依赖ParamSpec的参数排除特性,具体实现分版本:
方式1:Python 3.12+ 原生支持(推荐)
Python 3.12引入了ParamSpec.exclude方法,可直接从参数规范中移除指定的仅关键字参数,主流类型检查器(如mypy、pyright)已支持该特性。
修正后的装饰器注解:
from typing import Callable, TypeVar, ParamSpec, Awaitable P = ParamSpec("P") T = TypeVar("T") async def to_be_wrapped(x: int, y: str, *, z: dict) -> str: return str(x) + str(y) + str(z) def force_z( func: Callable[P, Awaitable[T]] ) -> Callable[P.exclude("z"), Awaitable[T]]: async def wrapped(*args: P.args, **kwargs: P.kwargs): return await func(*args, **kwargs, z={"foo": "bar"}) return wrapped async def main(): await force_z(to_be_wrapped)(1, "") # 无类型错误
解释
P.exclude("z")明确告知类型检查器:被装饰函数需要z这个仅关键字参数,但装饰器会自动注入它,因此调用装饰后的函数时无需传入z。- 此方式代码简洁,完全符合PEP后续扩展的类型注解规范。
方式2:兼容Python <3.12版本
若环境不支持ParamSpec.exclude,可使用typing_extensions提供的ParamSpecExclude工具:
from typing import Callable, TypeVar, ParamSpec, Awaitable from typing_extensions import ParamSpecExclude P = ParamSpec("P") T = TypeVar("T") async def to_be_wrapped(x: int, y: str, *, z: dict) -> str: return str(x) + str(y) + str(z) def force_z( func: Callable[P, Awaitable[T]] ) -> Callable[ParamSpecExclude(P, "z"), Awaitable[T]]: async def wrapped(*args: P.args, **kwargs: P.kwargs): return await func(*args, **kwargs, z={"foo": "bar"}) return wrapped async def main(): await force_z(to_be_wrapped)(1, "")
总结
- PEP 612最初的
Concatenate只覆盖位置参数场景,仅关键字参数的注解支持是后续通过ParamSpec.exclude补充的。 - 优先使用Python 3.12+的原生特性,代码更易维护且兼容性更好。
内容的提问来源于stack exchange,提问作者nicoco
相关产品推荐
相关产品推荐

