使用 TypeVar 在 MyPy 中键入带有参数的装饰器会产生预期的无人居住类型

Typing Decorator with Parameters in MyPy with TypeVar yields expected uninhabited type

MyPy 在 Callable *args**kwargs 方面存在一些问题,特别是关于装饰器的问题,详见:https://github.com/python/mypy/issues/1927

具体来说,对于只包装函数(并且不更改其签名)的无参数装饰器,您需要以下内容:

from typing import Any, Callable, cast, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(func: FuncT) -> FuncT:
    def wrapped(*args, **kwargs):
        print("Running", func.__name__)
        return func(*args, **kwargs)
    return cast(FuncT, wrapped)

末尾的 cast() 应该是不必要的(MyPy 应该能够通过调用 wrapped 末尾的 func 来推导 wrapped 确实是 FuncT -> FuncT).我可以忍受这个直到它被修复。

然而,当您引入带参数的装饰器时,这会很糟糕。考虑装饰器:

def print_on_call(foo):
    def decorator(func):
        def wrapped(*args, **kwargs):
            print("Running", foo)
            return func(*args, **kwargs)
        return wrapped
    return decorator

这样使用:

@print_on_call('bar')
def stuff(a, b):
    return a + b

我们可能会尝试键入它(使用 Guido 认可的无参数示例作为指导),如下所示:

from typing import Any, Callable, Dict, List, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
    def decorator(func: FuncT) -> FuncT:
        def wrapped(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
            print("Running", foo)
            return func(*args, **kwargs)
        return cast(FuncT, wrapped)
    return cast(Callable[[FuncT], FuncT], decorator)

这似乎是类型检查,但当我们使用它时:

@print_on_call('bar')
def stuff(a: int, b: int) -> int:
    return a + b

我们收到一个严重的错误:

error: Argument 1 has incompatible type Callable[[int, int], int]; expected <uninhabited>

我对这怎么可能感到有点困惑。正如 PEP 484 中所讨论的那样,Callable[[int, int], int] 似乎应该是 Callable[..., Any] 的子类型。

我假设这可能是在 print_on_call 的 return 类型和参数之间使用泛型和 return 类型到 decorator 之间的错误迭代,所以我把我的例子削减到最低限度(虽然不再是一个工作装饰器,它仍然应该进行类型检查):

from typing import Any, Callable, Dict, List, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
    return cast(Callable[[FuncT], FuncT], None)

然而,这仍然会导致上述错误。如果 #type: ignore 离开,我会觉得这没什么问题,但不幸的是,由于这个问题,任何用这个装饰器装饰的函数都有类型 <uninhabited>,所以你开始到处失去类型安全。

都说了 (tl;dr):

如何输入带参数的装饰器(不修改函数的签名)?以上是错误吗?可以解决吗?

MyPy 版本:0.501(本文发布时的最新版本)

糟糕!看来我搜索的不够努力。已经存在问题和解决方法:https://github.com/python/mypy/issues/1551#issuecomment-253978622

现在mypy直接支持了:

https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators

FuncT = TypeVar("FuncT", bound=Callable[..., Any]) 

def my_decorator(func: FuncT) -> FuncT:
    @wraps(func)
    def wrapped(*args: Any, **kwargs: Any) -> Any:
        print("something")
        return func(*args, **kwargs)
    return cast(FuncT, wrapped)