装饰器的 Python 3 类型提示

Posted

技术标签:

【中文标题】装饰器的 Python 3 类型提示【英文标题】:Python 3 type hinting for decorator 【发布时间】:2018-04-14 01:58:06 【问题描述】:

考虑以下代码:

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

这段代码按预期工作。现在想象一下我想扩展它,而不是仅仅执行func(*args, **kwargs) 我想在参数中注入用户名。因此,我修改了函数签名。

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

我无法找到正确的输入方式。我知道在这个例子中,装饰函数和返回函数在技术上应该具有相同的签名(但即使这样也没有被检测到)。

【问题讨论】:

可调用参数真的很难输入;各种提案四处流传,但目前,我唯一能告诉你的是继续使用... 我也是这么想的。我没有找到任何东西,除了在 GitHub 上打开的一些问题,建议添加诸如 StarArgStarKwarg 之类的类型。我想知道在这种非常特殊的情况下是否有解决方案,但我认为不会有任何解决方案:( 您发现类型提示开发人员正在进行讨论以改善这种情况。 【参考方案1】:

您不能使用Callable 来说明其他参数;它们不是通用的。您唯一的选择是说您的装饰器采用 Callable 并返回不同的 Callable

在您的情况下,您可以使用 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:
            # ...

即使这样,当您使用 reveal_type() 时,生成的装饰 foo() 函数的打字签名为 def (*Any, **Any) -&gt; builtins.bool*

目前正在讨论各种建议以使Callable 更加灵活,但这些建议尚未实现。见

Allow variadic generics Proposal: Generalize Callable to be able to specify argument names and kinds TypeVar to represent a Callable's arguments Support function decorators excellently

举一些例子。该列表中的最后一个是包含您的特定用例的总括票据,即更改可调用签名的装饰器:

与返回类型或参数混淆

对于任意函数,您根本无法做到这一点——甚至没有语法。这是我为它编造的一些语法。

【讨论】:

【参考方案2】:

PEP 612 在接受答案后被接受,我们现在在 Python 3.10 中有 typing.ParamSpectyping.Concatenate。使用这些变量,我们可以正确地键入一些操纵位置参数的装饰器。

请注意,mypy 对 PEP 612 的支持仍在进行中 (tracking issue)。

有问题的代码可以这样输入(尽管由于上述原因未在 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

【讨论】:

【参考方案3】:

我在 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

【讨论】:

以上是关于装饰器的 Python 3 类型提示的主要内容,如果未能解决你的问题,请参考以下文章

Python进阶装饰器(Decorator)

Python基础装饰器

面试题新

Python 3.5(装饰器)

python高阶3 python装饰器

装饰器