You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 14:23:11