简体   繁体   English

Python 装饰器的 3 种提示

[英]Python 3 type hinting for decorator

Consider the following code:考虑以下代码:

from typing import Callable, Any

TFunc = Callable[..., Any]

def get_authenticated_user(): return "John"

def require_auth() -> Callable[TFunc, TFunc]:
    def decorator(func: TFunc) -> TFunc:
        def wrapper(*args, **kwargs) -> Any:
            user = get_authenticated_user()
            if user is None:
                raise Exception("Don't!")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_auth()
def foo(a: int) -> bool:
    return bool(a % 2)

foo(2)      # Type check OK
foo("no!")  # Type check failing as intended

This piece of code is working as intended.这段代码按预期工作。 Now imagine I want to extend this, and instead of just executing func(*args, **kwargs) I want to inject the username in the arguments. Therefore, I modify the function signature.现在想象一下我想扩展它,而不是仅仅执行func(*args, **kwargs)我想在 arguments 中注入用户名。因此,我修改了 function 签名。

from typing import Callable, Any

TFunc = Callable[..., Any]

def get_authenticated_user(): return "John"

def inject_user() -> Callable[TFunc, TFunc]:
    def decorator(func: TFunc) -> TFunc:
        def wrapper(*args, **kwargs) -> Any:
            user = get_authenticated_user()
            if user is None:
                raise Exception("Don't!")
            return func(*args, user, **kwargs)  # <- call signature modified

        return wrapper

    return decorator


@inject_user()
def foo(a: int, username: str) -> bool:
    print(username)
    return bool(a % 2)


foo(2)      # Type check OK
foo("no!")  # Type check OK <---- UNEXPECTED

I can't figure out a correct way to type this.我想不出正确的输入方式。 I know that on this example, decorated function and returned function should technically have the same signature (but even that is not detected).我知道在这个例子中,装饰 function 并返回 function 在技术上应该具有相同的签名(但即使这样也没有被检测到)。

You can't use Callable to say anything about additional arguments;你不能使用Callable来说明额外的参数; they are not generic.它们不是通用的。 Your only option is to say that your decorator takes a Callable and that a different Callable is returned.您唯一的选择是说您的装饰器接受一个Callable并返回一个不同的Callable

In your case you can nail down the return type with a typevar:在您的情况下,您可以使用 typevar 确定返回类型:

RT = TypeVar('RT')  # return type

def inject_user() -> Callable[[Callable[..., RT]], Callable[..., RT]]:
    def decorator(func: Callable[..., RT]) -> Callable[..., RT]:
        def wrapper(*args, **kwargs) -> RT:
            # ...

Even then the resulting decorated foo() function has a typing signature of def (*Any, **Any) -> builtins.bool* when you use reveal_type() .即便如此,当您使用reveal_type()时,生成的装饰foo()函数的类型签名为def (*Any, **Any) -> builtins.bool* reveal_type() def (*Any, **Any) -> builtins.bool*

Various proposals are currently being discussed to make Callable more flexible but those have not yet come to fruition.目前正在讨论各种提案,以使Callable更加灵活,但尚未取得成果。 See

for some examples.对于一些例子。 The last one in that list is an umbrella ticket that includes your specific usecase, the decorator that alters the callable signature:该列表中的最后一个是包含您的特定用例的伞式票证,即更改可调用签名的装饰器:

Mess with the return type or with arguments弄乱返回类型或参数

For an arbitrary function you can't do this at all yet -- there isn't even a syntax.对于任意函数,您根本无法执行此操作 - 甚至没有语法。 Here's me making up some syntax for it.这是我为它编写的一些语法。

PEP 612 was accepted after the accepted answer, and we now have typing.ParamSpec and typing.Concatenate in Python 3.10. PEP 612在接受答案后被接受,我们现在在 Python 3.10 中拥有typing.ParamSpectyping.Concatenate With these variables, we can correctly type some decorators that manipulate positional parameters.有了这些变量,我们就可以正确键入一些操纵位置参数的装饰器。

Note that mypy's support for PEP 612 is still under way ( tracking issue ).请注意, mypy 对 PEP 612 的支持仍在进行中跟踪问题)。

The code in question can be typed like this (though not tested on mypy for the reason above)有问题的代码可以这样输入(尽管由于上述原因未在 mypy 上进行测试)

from typing import Callable, ParamSpec, Concatenate, TypeVar

Param = ParamSpec("Param")
RetType = TypeVar("RetType")
OriginalFunc = Callable[Param, RetType]
DecoratedFunc = Callable[Concatenate[Param, str], RetType]

def get_authenticated_user(): return "John"

def inject_user() -> Callable[[OriginalFunc], DecoratedFunc]:
    def decorator(func: OriginalFunc) -> DecoratedFunc:
        def wrapper(*args, **kwargs) -> RetType:
            user = get_authenticated_user()
            if user is None:
                raise Exception("Don't!")
            return func(*args, user, **kwargs)  # <- call signature modified

        return wrapper

    return decorator


@inject_user()
def foo(a: int, username: str) -> bool:
    print(username)
    return bool(a % 2)


foo(2)      # Type check OK
foo("no!")  # Type check should fail

I tested this in Pyright.我在 Pyright 中对此进行了测试。

from typing import Any, Callable, Type, TypeVar

T = TypeVar('T')

def typing_decorator(rtype: Type[T]) -> Callable[..., Callable[..., T]]:
    """
    Useful function to typing a previously decorated func.
    ```
    @typing_decorator(rtype = int)
    @my_decorator()
    def my_func(a, b, *c, **d):
        ...
    ```
    In Pyright the return typing of my_func will be int.
    """
    def decorator(function: Any) -> Any:
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            return function(*args, **kwargs)
        return wrapper
    return decorator  # type: ignore

The problem is solved using the decohints library:使用decohints库解决了这个问题:

pip install decohints

Here is how it will work with your code:以下是它如何处理您的代码:

from decohints import decohints


def get_authenticated_user():
    return "John"


@decohints
def inject_user():
    def decorator(func):
        def wrapper(*args, **kwargs):
            user = get_authenticated_user()
            if user is None:
                raise Exception("Don't!")
            return func(*args, user, **kwargs)  # <- call signature modified

        return wrapper

    return decorator


@inject_user()
def foo(a: int, username: str) -> bool:
    print(username)
    return bool(a % 2)

If you type below foo() in PyCharm and wait, it will show foo function parameter hints (a: int, username: str) .如果您在 PyCharm 中的foo()下方键入并等待,它将显示foo function parameter hints (a: int, username: str)

Here is a link to the decohints sources, there are also other options for solving this problem: https://github.com/gri-gus/decohints这是decohints源的链接,还有其他解决此问题的选项: https://github.com/gri-gus/decohints

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM