预定义函数的 ParamSpec,不使用通用 Callable[P]

HTE*_*HTE 5 python python-typing

我想为已知函数编写一个包装函数,例如

    def wrapper(*args, **kwargs)
         foo()
         return known_function(*args, **kwargs)
Run Code Online (Sandbox Code Playgroud)

我如何添加类型注释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)
Run Code Online (Sandbox Code Playgroud)

我可以强制P只对 有效known_function,而不将其链接到Callable- 参数吗?

Dan*_*erg 4

PEP 612 以及文档ParamSpec.argsParamSpec.kwargs的文档对此非常清楚:

\n
\n
\n

这些 \xe2\x80\x9cproperties\xe2\x80\x9d 只能用作 和 的带注释类型*args**kwargsParamSpec已在范围内访问。

\n
\n

- 来源:PEP 612(“ParamSpec 的组成部分”->“有效使用位置”)

\n
\n
\n

这两个属性都要求带注释的参数位于范围内。

\n
\n

- 来源:python.typing模块文档 ( class typing.ParamSpec-> args/kwargs )

\n
\n
\n

它们[参数规范]仅在使用时有效Concatenate,或作为第一个参数Callable,或作为用户定义的泛型的参数。

\n
\n

- 来源:python.typing模块文档(class typing.ParamSpec,第二段)

\n
\n

所以不,你不能使用参数规范args/ kwargs,而不将其绑定为具体的Callable你想要使用它们的范围内。

\n

我想知道你为什么想要那个。如果你知道总会打电话wrapperknown_function)具有完全相同的参数,那么您只需用相同的参数对其进行注释即可。例子:

\n
def known_function(x: int, y: str) -> bool:\n    return str(x) == y\n\n\ndef wrapper(x: int, y: str) -> bool:\n    # other things...\n    return known_function(x, y)\n
Run Code Online (Sandbox Code Playgroud)\n

如果您确实wrapper接受除了传递给 的参数之外的其他known_function参数,那么您也只需包含这些参数:

\n
def known_function(x: int, y: str) -> bool:\n    return str(x) == y\n\n\ndef wrapper(a: float, x: int, y: str) -> bool:\n    print(a ** 2)\n    return known_function(x, y)\n
Run Code Online (Sandbox Code Playgroud)\n

如果你的论点是你不想重复自己,因为known_function有 42 个不同且类型复杂的参数,那么(恕我直言)known_function应该被涂上大量汽油并点燃。

\n
\n

如果您坚持动态关联参数规范(或者出于学术原因对可能的解决方法感到好奇),那么以下是我能想到的最好的事情。

\n

您编写了一个受保护的装饰器,该装饰器仅用于known_function. (如果用其他任何东西调用它来保护您自己的理智,您甚至可以引发异常。)您在该装饰器定义包装器(并添加任何其他参数,如果您需要的话)。因此,您将能够使用装饰函数的*args/来注释它的/ 。在这种情况下,您可能不想使用,因为您从该装饰器收到的函数可能不是为了替换,而是与它并排。**kwargsParamSpecArgsParamSpecKwargsfunctools.wrapsknown_function

\n

这是一个完整的工作示例:

\n
from collections.abc import Callable\nfrom typing import Concatenate, ParamSpec, TypeVar\n\n\nP = ParamSpec("P")\nT = TypeVar("T")\n\n\ndef known_function(x: int, y: str) -> bool:\n    """Does thing XY"""\n    return str(x) == y\n\n\ndef _decorate(f: Callable[P, T]) -> Callable[Concatenate[float, P], T]:\n    if f is not known_function:  # type: ignore[comparison-overlap]\n        raise RuntimeError("This is an exclusive decorator.")\n\n    def _wrapper(a: float, /, *args: P.args, **kwargs: P.kwargs) -> T:\n        """Also does thing XY, but first does something else."""\n        print(a ** 2)\n        return f(*args, **kwargs)\n    return _wrapper\n\n\nwrapper = _decorate(known_function)\n\n\nif __name__ == "__main__":\n    print(known_function(1, "2"))\n    print(wrapper(3.14, 10, "10"))\n
Run Code Online (Sandbox Code Playgroud)\n

输出如预期:

\n
\n假\n9.8596\n真\n
\n

添加reveal_type(wrapper)到脚本并运行mypy会得到以下结果:

\n
\n显示的类型为“def (builtins.float, x:builtins.int, y:builtins.str) ->builtins.bool”\n
\n

PyCharm 还提供了有关函数签名的相关建议,它是通过known_function传递到_decorate.

\n

但再次强调一下,我认为这不是一个好的设计。如果您的“包装器”不是通用的,而是始终调用相同的函数,则应该显式注释它,以便其参数与该函数相对应。毕竟:

\n
\n

显式的比隐式的好。

\n
\n

- Python 之禅,第 2 行

\n

  • 不需要粗俗的语言。`ParamSpec` 似乎是针对这个用例而设计的,但忘记了函数已知的简单情况。 (3认同)
  • 谢谢,按预期工作。pycharm 和 mypy 都会按预期建议并捕获错误。用例就像你说的那样,避免在known_function中重复42个参数,但我无法控制它的上游定义,因为它来自第三方库。因此首先需要包装器。 (2认同)