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

问题描述 投票:0回答:2

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

    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
参数吗?

python python-typing
2个回答
4
投票

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 之禅

,第 2 行


3
投票
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 可以正确推导参数。正如你所看到的,我们甚至没有使用该函数,我们用 
_

标记它。我们仅将其用于类型提示。

    

© www.soinside.com 2019 - 2024. All rights reserved.