Python 3的类型提示用于装饰器。

67

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

这段代码按预期工作。现在想象一下,我想要扩展它,不仅仅执行 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

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


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

71

PEP 612在接受答案后被接受,现在我们在Python 3.10中拥有typing.ParamSpectyping.Concatenate。通过这些变量,我们可以正确地对一些操作位置参数的装饰器进行类型注解。

请注意,mypy对PEP 612的支持仍在进行中跟踪问题)。

有关问题的代码可以像这样进行类型注解(尽管由于上述原因未经mypy测试)

from typing import Callable, ParamSpec, Concatenate, TypeVar

Param = ParamSpec("Param")
RetType = TypeVar("RetType")
OriginalFunc = Callable[Param, RetType]
DecoratedFunc = Callable[Concatenate[str, Param], 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

现在,由于mypy和Python 3.10已经支持了这个功能,我尝试了一下,但是在Callable声明的类型注释上(Param = ParamSpec("Param"); MyFunc = Callable[Param, RetType]),我遇到了这个错误:mypy:error: The first argument to Callable must be a list of types, parameter specification, or "..." - gertvdijk
5
Concatenate 函数有一个错误,应该是 Concatenate[str, Param]ParamSpec 必须放在最后。 - iuvbio
2
跟踪 PEP 612 支持的 mypy 问题现在已经修复并关闭。 - David Pärsson
包装函数的参数仍然需要进行类型注解(至少在我的情况下),如下所示:def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> RetType: - undefined

59

你无法使用 Callable 来表示额外参数,因为它们不是通用的。你唯一的选择就是说你的装饰器接受一个 Callable,并返回一个不同的 Callable

在你的情况下,你可以使用类型变量来确切指定返回类型:

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) -> builtins.bool

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

有一些示例。 该列表中的最后一个是一个包括您特定用例的总体票,即修改可调用签名的装饰器:

 

更改返回类型或参数

    

对于任意函数,您都无法完成此操作-甚至没有语法。 这是我为其编写的一些语法。


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

-4
问题可以使用decohints库来解决:
pip install decohints

下面是如何在您的代码中运行:

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)

如果您在PyCharm中输入下面的foo()并等待,它将显示foo函数的参数提示(a: int, username: str)
这里有一个指向decohints源代码的链接,还有其他解决此问题的选项:https://github.com/gri-gus/decohints

18
为了方便读者,以下是“decohints”的完整实现代码:def decohints(decorator: Callable) -> Callable: return decorator该函数接受一个装饰器作为参数,并返回该装饰器本身。 - David Röthlisberger
4
@DavidRöthlisberger 哈哈 - iuvbio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接