在我的项目中使用
mypy
时遇到问题。首先,我使用 backoff
包对某些函数/方法进行一些重试。然后我意识到,大多数选项只是重复的,因此我创建了每个项目的装饰器,并为 backoff
装饰器填充了所有常见值。但是,我不知道如何注释这样的“装饰器中的装饰器”。更重要的是,这应该与同步/异步函数/方法矩阵一起使用。这是重现我的痛苦的代码:
import asyncio
from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar
T = TypeVar("T")
P = ParamSpec("P")
AnyCallable = Callable[P, T | Awaitable[T]]
Decorator = Callable[[AnyCallable[P, T]], AnyCallable[P, T]]
def third_party_decorator(
a: int | None = None,
b: str | None = None,
c: Literal[None] = None,
d: bool | None = None,
e: str | None = None,
) -> Decorator[P, T]:
def decorator(f: AnyCallable[P, T]) -> AnyCallable[P, T]:
@wraps(f)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | Awaitable[T]:
print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
return f(*args, **kwargs)
return wrapper
return decorator
def parametrized_decorator(f: AnyCallable[P, T] | None = None, **kwargs: Any) -> Decorator[P, T] | AnyCallable[P, T]:
def decorator(f: AnyCallable[P, T]) -> AnyCallable[P, T]:
defaults = {"a": 1, "b": "b", "c": None, "d": True}
defaults.update(kwargs)
print(f"parametrized_decorator {f = } {defaults = }")
decorator: Decorator[P, T] = third_party_decorator(**defaults)
wrapped = decorator(f)
return wrapped
if f is None:
return decorator
else:
return decorator(f)
@parametrized_decorator
def sync_straight_function(x: int = 0) -> None:
print(f"sync_straight_function {x = }")
@parametrized_decorator(b="B", e="e")
def sync_parametrized_function(x: str = "abc", y: bool = False) -> None:
print(f"sync_parametrized_function {x = } {y = }")
@parametrized_decorator
async def async_straight_function(x: int = 0) -> None:
print(f"sync_straight_function {x = }")
@parametrized_decorator(b="B", e="e")
async def async_parametrized_function(x: str = "abc", y: bool = False) -> None:
print(f"sync_parametrized_function {x = } {y = }")
class Foo:
@parametrized_decorator
def sync_straight_method(self, x: int = 0) -> None:
print(f"sync_straight_function {x = }")
@parametrized_decorator(b="B", e="e")
def sync_parametrized_method(self, x: str = "abc", y: bool = False) -> None:
print("sync_parametrized_method", x, y)
@parametrized_decorator
async def async_straight_method(self, x: int = 0) -> None:
print(f"sync_straight_function {x = }")
@parametrized_decorator(b="B", e="e")
async def async_parametrized_method(self, x: str = "abc", y: bool = False) -> None:
print(f"sync_parametrized_function {x = } {y = }")
def main_sync_functions() -> None:
sync_straight_function()
sync_straight_function(1)
sync_parametrized_function()
sync_parametrized_function("xyz", True)
async def main_async_functions() -> None:
await async_straight_function()
await async_straight_function(1)
await async_parametrized_function()
await async_parametrized_function("xyz", True)
def main_sync_methods() -> None:
foo = Foo()
foo.sync_straight_method()
foo.sync_straight_method(1)
foo.sync_parametrized_method()
foo.sync_parametrized_method("xyz", True)
async def main_async_methods() -> None:
foo = Foo()
await foo.async_straight_method()
await foo.async_straight_method(1)
await foo.async_parametrized_method()
await foo.async_parametrized_method("xyz", True)
if __name__ == "__main__":
print("\nSYNC FUNCTIONS:")
main_sync_functions()
print("\nASYNC FUNCTIONS:")
asyncio.run(main_async_functions())
print("\nSYNC METHODS:")
main_sync_methods()
print("\nASYNC METHODS:")
asyncio.run(main_async_methods())
mypy 的输出有 44 个错误:
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "int | None" [arg-type]
decorator: Decorator[P, T] = third_party_decorator(**defaults)
^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "str | None" [arg-type]
decorator: Decorator[P, T] = third_party_decorator(**defaults)
^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "None" [arg-type]
decorator: Decorator[P, T] = third_party_decorator(**defaults)
^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "bool | None" [arg-type]
decorator: Decorator[P, T] = third_party_decorator(**defaults)
^~~~~~~~
parametrized-decorator-typing.py:48: error: Argument 1 has incompatible type "Callable[[str, bool], None]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]" [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:48: error: Argument 1 has incompatible type "Callable[[str, bool], None]"; expected <nothing> [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:53: error: Argument 1 to "parametrized_decorator" has incompatible type "Callable[[int], Coroutine[Any, Any, None]]"; expected
"Callable[[int], <nothing> | Awaitable[<nothing>]] | None" [arg-type]
@parametrized_decorator
^
parametrized-decorator-typing.py:58: error: Argument 1 has incompatible type "Callable[[str, bool], Coroutine[Any, Any, None]]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]" [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:58: error: Argument 1 has incompatible type "Callable[[str, bool], Coroutine[Any, Any, None]]"; expected <nothing> [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:68: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], None]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]" [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:68: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], None]"; expected <nothing> [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:72: error: Argument 1 to "parametrized_decorator" has incompatible type "Callable[[Foo, int], Coroutine[Any, Any, None]]"; expected
"Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | None" [arg-type]
@parametrized_decorator
^
parametrized-decorator-typing.py:76: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], Coroutine[Any, Any, None]]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]" [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:76: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], Coroutine[Any, Any, None]]"; expected <nothing> [arg-type]
@parametrized_decorator(b="B", e="e")
^
parametrized-decorator-typing.py:82: error: Too few arguments [call-arg]
sync_straight_function()
^~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:83: error: Argument 1 has incompatible type "int"; expected "Callable[[int], Awaitable[None] | None]" [arg-type]
sync_straight_function(1)
^
parametrized-decorator-typing.py:85: error: "Awaitable[<nothing>]" not callable [operator]
sync_parametrized_function()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:86: error: "Awaitable[<nothing>]" not callable [operator]
sync_parametrized_function("xyz", True)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:86: error: Argument 1 has incompatible type "str"; expected <nothing> [arg-type]
sync_parametrized_function("xyz", True)
^~~~~
parametrized-decorator-typing.py:86: error: Argument 2 has incompatible type "bool"; expected <nothing> [arg-type]
sync_parametrized_function("xyz", True)
^~~~
parametrized-decorator-typing.py:90: error: Incompatible types in "await" (actual type "Callable[[int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]") [misc]
await async_straight_function()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:90: error: Too few arguments [call-arg]
await async_straight_function()
^~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:91: error: Incompatible types in "await" (actual type "Callable[[int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]") [misc]
await async_straight_function(1)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:91: error: Argument 1 has incompatible type "int"; expected "Callable[[int], <nothing> | Awaitable[<nothing>]]" [arg-type]
await async_straight_function(1)
^
parametrized-decorator-typing.py:93: error: "Awaitable[<nothing>]" not callable [operator]
await async_parametrized_function()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:94: error: "Awaitable[<nothing>]" not callable [operator]
await async_parametrized_function("xyz", True)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:94: error: Argument 1 has incompatible type "str"; expected <nothing> [arg-type]
await async_parametrized_function("xyz", True)
^~~~~
parametrized-decorator-typing.py:94: error: Argument 2 has incompatible type "bool"; expected <nothing> [arg-type]
await async_parametrized_function("xyz", True)
^~~~
parametrized-decorator-typing.py:99: error: Too few arguments [call-arg]
foo.sync_straight_method()
^~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:100: error: Argument 1 has incompatible type "int"; expected "Callable[[Foo, int], Awaitable[None] | None]" [arg-type]
foo.sync_straight_method(1)
^
parametrized-decorator-typing.py:100: error: Argument 1 has incompatible type "int"; expected "Foo" [arg-type]
foo.sync_straight_method(1)
^
parametrized-decorator-typing.py:102: error: "Awaitable[<nothing>]" not callable [operator]
foo.sync_parametrized_method()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:103: error: "Awaitable[<nothing>]" not callable [operator]
foo.sync_parametrized_method("xyz", True)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:103: error: Argument 1 has incompatible type "str"; expected <nothing> [arg-type]
foo.sync_parametrized_method("xyz", True)
^~~~~
parametrized-decorator-typing.py:103: error: Argument 2 has incompatible type "bool"; expected <nothing> [arg-type]
foo.sync_parametrized_method("xyz", True)
^~~~
parametrized-decorator-typing.py:108: error: Incompatible types in "await" (actual type "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]") [misc]
await foo.async_straight_method()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:108: error: Too few arguments [call-arg]
await foo.async_straight_method()
^~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:109: error: Incompatible types in "await" (actual type "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]") [misc]
await foo.async_straight_method(1)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:109: error: Argument 1 has incompatible type "int"; expected "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]]" [arg-type]
await foo.async_straight_method(1)
^
parametrized-decorator-typing.py:109: error: Argument 1 has incompatible type "int"; expected "Foo" [arg-type]
await foo.async_straight_method(1)
^
parametrized-decorator-typing.py:111: error: "Awaitable[<nothing>]" not callable [operator]
await foo.async_parametrized_method()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:112: error: "Awaitable[<nothing>]" not callable [operator]
await foo.async_parametrized_method("xyz", True)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:112: error: Argument 1 has incompatible type "str"; expected <nothing> [arg-type]
await foo.async_parametrized_method("xyz", True)
^~~~~
parametrized-decorator-typing.py:112: error: Argument 2 has incompatible type "bool"; expected <nothing> [arg-type]
await foo.async_parametrized_method("xyz", True)
^~~~
Found 44 errors in 1 file (checked 1 source file)
我最终得到了下面的代码。不要介意
type: ignore
,因为这是第三方装饰器。
AnyCallable = Callable[..., Any]
F = TypeVar("F", bound=AnyCallable)
Decorator = Callable[[F], F]
def third_party_decorator(
a: int | None = None,
b: str | None = None,
c: Literal[None] = None,
d: bool | None = None,
e: str | None = None,
) -> Decorator[F]:
def decorator(f: F) -> F:
@wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
return f(*args, **kwargs)
return wrapper # type: ignore
return decorator
def parametrized_decorator(f: AnyCallable | None = None, **kwargs: Any) -> AnyCallable:
def decorator(f: F) -> F:
defaults: dict[str, Any] = {"a": 1, "b": "b", "c": None, "d": True}
defaults.update(kwargs)
print(f"parametrized_decorator {f = } {defaults = }")
decorator: Decorator[F] = third_party_decorator(**defaults)
wrapped = decorator(f)
return wrapped
if f is None:
return decorator
else:
return decorator(f)
您提出的解决方案有一个主要缺陷:您的
parametrized_decorator
破坏了函数签名。您仍然需要一个 typevar 来保存它。
首先,你的
T
没有任何界限,所以 Awaitable[Something]
也可以只是 T
。在你的 third_party_decorator
中,wrapper
返回 T | Awaitable[T]
,这很奇怪 - 它总是返回给定的 T
,它不能变得更加等待。所以,我们可以在这里完全忘记Awaitable
,这大大简化了打字。
然后,你的装饰器可以有两种不同的行为方式:当 func 未传递时,要么是二阶装饰器(返回装饰器),要么返回一个函数。这可以表示为
overload
。这是我的建议(游乐场):
import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar, overload
T = TypeVar("T")
_T1 = TypeVar("_T1")
P = ParamSpec("P")
_P1 = ParamSpec("_P1")
def third_party_decorator(
a: int | None = None,
b: str | None = None,
c: Literal[None] = None,
d: bool | None = None,
e: str | None = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
def decorator(f: Callable[P, T]) -> Callable[P, T]:
@wraps(f)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
return f(*args, **kwargs)
return wrapper
return decorator
@overload
def parametrized_decorator(f: None = ..., /, **kwargs: Any) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
@overload
def parametrized_decorator(f: Callable[P, T], /, **kwargs: Any) -> Callable[P, T]: ...
def parametrized_decorator(f: Callable[P, T] | None = None, /, **kwargs: Any) -> Callable[[Callable[_P1, _T1]], Callable[_P1, _T1]] | Callable[P, T]:
def decorator(f: Callable[P, T]) -> Callable[P, T]:
defaults = {"a": 1, "b": "b", "c": None, "d": True}
defaults.update(kwargs)
print(f"parametrized_decorator {f = } {defaults = }")
decorator = third_party_decorator(**defaults) # type: ignore[arg-type]
wrapped = decorator(f)
return wrapped
if f is None:
# You can avoid type-ignore here, but this will require an insane if
# clause with `decorator` body effectively repeated twice
return decorator # type: ignore[return-value]
else:
return decorator(f)
注意到上面丑陋的
_T1
和_P1
了吗?这是因为可调用本身是通用的,并且绑定得太早(P
和T
在新可调用的左侧和两侧应该相同)。让我们通过创建“替代可调用”来解决这个问题,它不是通用的,但仅提供通用的__call__
(playground):
import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar, overload, Protocol
T = TypeVar("T")
P = ParamSpec("P")
class Decorator(Protocol):
def __call__(self, func: Callable[P, T], /) -> Callable[P, T]: ...
def third_party_decorator(
a: int | None = None,
b: str | None = None,
c: Literal[None] = None,
d: bool | None = None,
e: str | None = None,
) -> Decorator:
def decorator(f: Callable[P, T]) -> Callable[P, T]:
@wraps(f)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
return f(*args, **kwargs)
return wrapper
return decorator
@overload
def parametrized_decorator(f: None = ..., /, **kwargs: Any) -> Decorator: ...
@overload
def parametrized_decorator(f: Callable[P, T], /, **kwargs: Any) -> Callable[P, T]: ...
def parametrized_decorator(f: Callable[P, T] | None = None, /, **kwargs: Any) -> Decorator | Callable[P, T]:
def decorator(f: Callable[P, T]) -> Callable[P, T]:
defaults = {"a": 1, "b": "b", "c": None, "d": True}
defaults.update(kwargs)
print(f"parametrized_decorator {f = } {defaults = }")
decorator = third_party_decorator(**defaults) # type: ignore[arg-type]
wrapped = decorator(f)
return wrapped
if f is None:
return decorator
else:
return decorator(f)
现在您没有任何忽略注释并且具有正确的返回类型。
为了解释我的“破坏签名”,
reveal_type(sync_straight_function)
或只是尝试sync_straight_function('this', 'is', 'still', 'allowed', 'why?') + 1
与您的答案中的实现并观察没有mypy错误。