带有参数的装饰器的 MyPy 错误

MyPy Errors for Decorator w/ Arguments

我正在尝试使用 MyPy 进行严格的类型检查,但我似乎无法让带有参数的装饰器在不抛出冲突错误的情况下工作。

根据文档和其他答案,我认为你应该有这样的东西:

from time import sleep 
from typing import TypeVar, Any, Callable, cast

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

def multi_try_if_error(n_tries: int, sleep_duration: int) -> F:
    def decorator(fn: F) -> F:
        def wrapper(*args, **kwargs):
            for _ in range(n_tries):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    caught = e
                    sleep(sleep_duration)
            raise ValueError(caught)
        return cast(F, wrapper)
    return cast(F, decorator)


# example usage — this is where the error is raised
@multi_try_if_error(n_tries=3, sleep_duration=1)
def query_db(q: str) -> None:
    return

但这会产生以下错误:

lib/decorator_defined.py:32: error: Function is missing a type annotation
lib/decorator_used.py:23: error: Untyped decorator makes function "query_db" untyped
lib/decorator_used.py:23: error: <nothing> not callable

尽管这不是文档所建议的,但我可以将包装器定义更改为:

def wrapper(*args: Any, **kwargs: Any) -> Any:
    for _ in range(n_tries):
        ...

这解决了第一个错误,但在使用装饰器的任何地方我仍然遇到其他两个装饰器错误。

关于如何解决这个问题有什么想法吗?

错误可以在 Mypy playground 上重现 here

这里的问题是装饰器外层的 return 类型。您需要将类型注释更正为以下内容:

from time import sleep 
from typing import TypeVar, Any, Callable, cast

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

def multi_try_if_error(n_tries: int, sleep_duration: int) -> Callable[[F], F]:
    def decorator(fn: F) -> F:
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            for _ in range(n_tries):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    caught = e
                    sleep(sleep_duration)
            raise ValueError(caught)
        return cast(F, wrapper)
    return decorator


@multi_try_if_error(n_tries=3, sleep_duration=1)
def query_db(q: str) -> None:
    return

这是怎么回事

概念化(和类型提示)一个不带参数的简单装饰器是相当简单的。我们定义了一个函数 C,它接受一个类型为 F 的函数,并吐出一个也是类型为 F.

的新函数
# Decorator that doesn't take arguments takes in a function,
# and spits out a function of the same type
DecoratorTypeNoArgs = Callable[[F], F]

然而,重要的是要认识到,那是而不是接受参数的装饰器所做的事情。 F 不是接受类型 F 的函数并吐出类型 F 的新函数(可以概念化为 Callable[[F], F]),而是 multi_try_if_error 是接受类型的函数两个 int 参数和 returns 一个函数,它将接收 F 类型的函数和 return 类型 F(可以概念化为Callable[[int, int], Callable[[F], F]])。

# Decorator that takes arguments takes in arguments,
# and spits out a decorator that doesn't take arguments
DecoratorTypeWithArgs = Callable[[int, int], Callable[[F], F]]

因此,装饰器的外层必须注释为 returning Callable[[F], F] 而不是 returning F。进行此更改后,您的装饰器 passes MyPy --strict 会大放异彩。

对于使用 Python >= 3.10 can be written a bit more succinct with the help of the new parameter specification variables 的人。

引用docs,这可以帮助我们解决“遗留”解决方案的以下问题:

  1. The type checker can’t type check the inner function because *args and **kwargs have to be typed Any.
  2. cast() may be required in the body of the add_logging decorator when returning the inner function, or the static type checker must be told to ignore the return inner.
from time import sleep
from typing import Callable, ParamSpec, TypeVar

T = TypeVar("T")
P = ParamSpec("P")


def multi_try_if_error(
    n_tries: int, sleep_duration: int
) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(fn: Callable[P, T]) -> Callable[P, T]:
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            for _ in range(n_tries):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    caught = e
                    sleep(sleep_duration)

            raise ValueError(caught)

        return wrapper

    return decorator