我想为已知函数编写一个包装函数,例如
def wrapper(*args, **kwargs)
foo()
return known_function(*args, **kwargs)
如何向
wrapper
添加类型注释,使其完全遵循 known_function
的类型注释
我已经看过了
ParamSpec
,但它似乎仅在包装函数是通用的并以内部函数作为参数时才有效。
P = ParamSpec("P")
T = TypeVar('T')
def wrapper(func_arg_that_i_dont_want: Callable[P,T], *args: P.args, **kwargs: P.kwargs)
foo()
return known_function(*args, **kwargs)
我可以强制
P
仅对 known_function
有效,而不将其链接到 Callable
参数吗?
PEP 612 以及
ParamSpec.args
和 ParamSpec.kwargs
的文档对此非常清楚:
这些“属性”只能用作
和*args
的注释类型,从范围内已有的**kwargs
访问。ParamSpec
- 来源:PEP 612(“ParamSpec 的组成部分”->“有效使用位置”)
这两个属性都要求带注释的参数位于范围内。
python.typing
模块文档 (class typing.ParamSpec
-> args
/kwargs
)
它们[参数规范]仅在
中使用时有效,或作为Concatenate
的第一个参数,或作为用户定义泛型的参数。Callable
python.typing
模块文档(class typing.ParamSpec
,第二段)
所以不,你不能使用参数规范
args
/kwargs
,除非在你想要使用它们的范围内将其绑定为具体的Callable
。
我怀疑你为什么想要那个。如果您知道
wrapper
将always调用known_function
并且您希望它(如您所说)具有完全相同的参数,那么您只需使用相同的参数对其进行注释即可。示例:
def known_function(x: int, y: str) -> bool:
return str(x) == y
def wrapper(x: int, y: str) -> bool:
# other things...
return known_function(x, y)
如果您确实希望
wrapper
接受除了传递给known_function
之外的其他参数,那么您也只需包含这些参数即可:
def known_function(x: int, y: str) -> bool:
return str(x) == y
def wrapper(a: float, x: int, y: str) -> bool:
print(a ** 2)
return known_function(x, y)
如果你的论点是你不想重复自己,因为
known_function
有 42 个不同且类型复杂的参数,那么(恕我直言)known_function
的设计应该被大量汽油覆盖并点燃。
如果您坚持动态关联参数规范(或者出于学术原因对可能的解决方法感到好奇),那么以下是我能想到的最好的事情。
您编写了一个受保护的装饰器,仅用于
known_function
。 (如果用其他任何东西调用它来保护您自己的理智,您甚至可以引发异常。)您可以在该装饰器中定义您的包装器(并添加任何其他参数,如果您需要的话)。因此,您将能够使用装饰函数的 *args
/**kwargs
来注释其 ParamSpecArgs
/ParamSpecKwargs
。在这种情况下,您可能不想想要使用
functools.wraps
,因为您从该装饰器接收到的函数可能不会取代
known_function
,而是与它并排。这是一个完整的工作示例:
from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def known_function(x: int, y: str) -> bool:
"""Does thing XY"""
return str(x) == y
def _decorate(f: Callable[P, T]) -> Callable[Concatenate[float, P], T]:
if f is not known_function: # type: ignore[comparison-overlap]
raise RuntimeError("This is an exclusive decorator.")
def _wrapper(a: float, /, *args: P.args, **kwargs: P.kwargs) -> T:
"""Also does thing XY, but first does something else."""
print(a ** 2)
return f(*args, **kwargs)
return _wrapper
wrapper = _decorate(known_function)
if __name__ == "__main__":
print(known_function(1, "2"))
print(wrapper(3.14, 10, "10"))
输出符合预期:
错误 9.8596 真的
将
reveal_type(wrapper)
添加到脚本并运行
mypy
给出以下内容:显示的类型为“def (builtins.float, x:builtins.int, y:builtins.str) ->builtins.bool”
PyCharm 还提供了有关函数签名的相关建议,这是通过将
known_function
传递到
_decorate
来推断的。但再次强调一下,我认为这不是一个好的设计。如果您的“包装器”不是通用的,而是始终调用相同的函数,则应该显式注释它,以便其参数与该函数相对应。毕竟:
显式优于隐式。
Python 之禅-
Concatenate
并稍微修改一下,我还没有找到针对这种情况的通用解决方案。这是
@Daniil Fajnberg
的上述答案的修改版本
from typing import TypeVar, ParamSpec, Callable
P = ParamSpec("P")
T = TypeVar("T")
S = TypeVar("S")
def paramspec_from(_: Callable[P, T]) -> Callable[[Callable[P, S]], Callable[P, S]]:
def _fnc(fnc: Callable[P, S]) -> Callable[P, S]:
return fnc
return _fnc
然后你可以像这样使用它:
@paramspec_from(known_function)
def wrapper(*args, **kwargs) -> string:
foo()
known_function(*args, **kwargs)
return "whatever"
问题是mypy只查看函数头来解析ParamSpec,而不是函数体,因此它无法推断出args和kwargs的类型。但是如果我们将函数提升到标头中,例如将其作为参数传递到装饰器中,mypy 可以正确推导参数。正如你所看到的,我们甚至没有使用该函数,我们用
_
标记它。我们仅将其用于类型提示。