装饰器的 Python 3 类型提示

新手上路,请多包涵

考虑以下代码:

 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

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

原文由 FunkySayu 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 875
2 个回答

您不能使用 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:
            # ...

即使这样,当您使用 --- 时,生成的装饰 foo() 函数的类型签名为 def (*Any, **Any) -> builtins.bool* reveal_type()

目前正在讨论各种提案,以使 Callable 更加灵活,但尚未实现。看

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

弄乱返回类型或参数

对于任意函数,您根本不能这样做——甚至没有语法。这是我为它编写的一些语法。

原文由 Martijn Pieters 发布,翻译遵循 CC BY-SA 3.0 许可协议

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[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

原文由 legogo 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题