如何将 ParamSpec 与方法装饰器一起使用?

Oca*_*b19 5 python typing python-3.10

我遵循PEP 0612(动机部分的最后一个)中的示例来创建一个可以向函数添加默认参数的装饰器。问题是,提供的示例仅适用于函数而不适用于方法,因为Concate不允许self在定义中的任何位置插入。

考虑这个例子,作为 PEP 中的例子的改编:

def with_request(f: Callable[Concatenate[Request, P], R]) -> Callable[P, R]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        return f(*args, request=Request(), **kwargs)

    return inner


class Thing:
    @with_request
    def takes_int_str(self, request: Request, x: int, y: str) -> int:
        print(request)
        return x + 7


thing = Thing()
thing.takes_int_str(1, "A")  # Invalid self argument "Thing" to attribute function "takes_int_str" with type "Callable[[str, int, str], int]"
thing.takes_int_str("B", 2)  # Argument 2 to "takes_int_str" of "Thing" has incompatible type "int"; expected "str"
Run Code Online (Sandbox Code Playgroud)

两次尝试都会引发 mypy 错误,因为与方法的第一个参数Request不匹配,如上所述。问题是不允许您附加到末尾,所以类似的东西也不起作用。selfConcatenateConcatenateRequestConcatenate[P, Request]

在我看来,这将是执行此操作的理想方法,但它不起作用,因为“Concatenate 的最后一个参数需要是 ParamSpec”。

def with_request(f: Callable[Concatenate[P, Request], R]) -> Callable[P, R]:
    ...


class Thing:
    @with_request
    def takes_int_str(self, x: int, y: str, request: Request) -> int:
        ...
Run Code Online (Sandbox Code Playgroud)

有任何想法吗?

rin*_*ngo 9

令人惊讶的是,网上对此的了解很少。我可以在Github上找到其他人对此的讨论python/typing,我使用你的示例进行了提炼。

这个解决方案的关键是CallbackProtocol,它在功能上等同于,另外还使我们能够像标准方法一样Callable修改返回类型__get__(本质上删除参数)。self

from __future__ import annotations

from typing import Any, Callable, Concatenate, Generic, ParamSpec, Protocol, TypeVar

from requests import Request

P = ParamSpec("P")
R = TypeVar("R", covariant=True)


class Method(Protocol, Generic[P, R]):
    def __get__(self, instance: Any, owner: type | None = None) -> Callable[P, R]:
        ...

    def __call__(self_, self: Any, *args: P.args, **kwargs: P.kwargs) -> R:
        ...


def request_wrapper(f: Callable[Concatenate[Any, Request, P], R]) -> Method[P, R]:
    def inner(self, *args: P.args, **kwargs: P.kwargs) -> R:
        return f(self, Request(), *args, **kwargs)

    return inner


class Thing:
    @request_wrapper
    def takes_int_str(self, request: Request, x: int, y: str) -> int:
        print(request)
        return x + 7


thing = Thing()
thing.takes_int_str(1, "a")
Run Code Online (Sandbox Code Playgroud)

由于 @Creris 询问了从 的定义引发的 mypy 错误inner,这是 mypy w/ParamSpecCallback Protocols中的一个明显错误mypy==0.991,因此这里是一个没有错误的替代实现:

from __future__ import annotations

from typing import Any, Callable, Concatenate, ParamSpec, TypeVar

from requests import Request

P = ParamSpec("P")
R = TypeVar("R", covariant=True)


def request_wrapper(f: Callable[Concatenate[Any, Request, P], R]) -> Callable[Concatenate[Any, P], R]:
    def inner(self: Any, *args: P.args, **kwargs: P.kwargs) -> R:
        return f(self, Request(), *args, **kwargs)

    return inner


class Thing:
    @request_wrapper
    def takes_int_str(self, request: Request, x: int, y: str) -> int:
        print(request)
        return x + 7


thing = Thing()
thing.takes_int_str(1, "a")

Run Code Online (Sandbox Code Playgroud)