我有一个工作函数,如下所示:

from time import monotonic
from itertools import chain
from datetime import timedelta
from typing import Callable, Sequence, Any, ParamSpec

P = ParamSpec('P')


def test_function(
    func: Callable[P, Any],
    args: list[P.args],  # Error: "args" member of ParamSpec is valid only when used with *args parameter
    kwargs: list[P.kwargs]  # Error: "kwargs" member of ParamSpec is valid only when used with **kwargs parameter
) -> None:
    for a, kw in zip(args, kwargs):
        args_str = ', '.join(chain(
            (str(i) for i in a),
            (f"{k}={v}" for k, v in kw.items())
        ))
        start = monotonic()
        func(*a, **kw)
        print(
            f"{func.__name__}({args_str}) "
            f"executed in: {timedelta(seconds=monotonic() - start)}"
        )

此函数的一些基本用法如下所示:

>>> def f1(a: int, b: float, *, c: int):
...     return a + b + c
...
>>> test_function(f1, [(1, 2), (3, 4)], [{'c': -1}, {'c': -2}])  # essentially no type hints here for args =(
f1(1, 2, c=-1) executed in: 0:00:00.000006
f1(3, 4, c=-2) executed in: 0:00:00.000004
>>>

它对我来说唯一的问题是它的argskwargs参数的类型提示.正如您所看到的,我试图使用ParamSpec,但是,它给出了一个错误(我在上面的代码注释中指出了这一点),并且在阅读了文档之后,我意识到,不幸的是,我不能随心所欲地使用这种类型:

它们仅在用于串联时有效,或用作Callable的第一个参数,或用作用户定义泛型的参数.

因此,我的问题是:有没有办法仍然验证包含几组参数的序列,每组参数都必须与函数签名匹配?

推荐答案

引用typing.ParamSpec年的文档:

P.args[...]应仅用于批注*args.P.kwargs[...]应仅用于批注**kwargs.

因此,这是不可能的ParamSpec.

能够在其他作用域中使用P.args/P.kwargs的 idea 是not new.但这其中存在一些根本性的问题,ParamSpec不太可能解决这些问题.实际上,PEP 612简要地提到了潜在的问题,解释了为什么首先引入ParamSpec:

这里的核心问题是,默认情况下,可以按位置调用Python中的参数,也可以将其作为关键字参数.这意味着我们实际上有三个类别(仅限位置、位置或关键字、仅限关键字),我们试图将其塞进两个类别.[.]从根本上说,为了在两个类别中都有一些东西时捕获两个类别,我们需要一个更高级别的原语(ParamSpec)来捕获所有这三个类别,然后将它们分开.

someone in the aforementioned issue人给出了一个很好的例子:

def foo(x: int) -> None: ...
def get_callable_and_args(x: Callable[P, Any]) -> Tuple[P.args, P.kwargs]: ...

reveal_type(get_callable_and_args(foo))  # <- What should this reveal?

我们应该显示一个int+空词典的元组吗?或者我们应该显示一个空的元组和一个从xint的单例字典?[...]在Python中,"位置或关键字"参数的存在使事情变得非常模糊.[.] 要完全解决这类问题,类型系统中需要有一种方法来分别表示仅位置参数、位置或关键字参数和仅关键字参数.但在这一点上,它将是一个不同的语言功能,而不再是ParamSpec.

因此,我不会在短期内屏息ParamSpec年来支持这一点.


如果我们只讨论positional个论点,你想要的TypeVarTuple个论点就有可能实现.PEP 646明确指出,它们可以用作Callable个论点.

这样的事情应该是可能的:

from collections.abc import Callable
from typing import Any, TypeVarTuple

Ts = TypeVarTuple("Ts")


def test_function(func: Callable[[*Ts], Any], args: list[tuple[*Ts]]) -> None:
    for a in args:
        func(*a)

我说should是因为从mypy does not fully support PEP 646 yet开始还很难核实.

但即便如此,也不支持关键字参数.基本上,只要出现三个不同参数类别中的combination,事情就会变得很棘手.

Python相关问答推荐

如何将ctyles.POINTER(ctyles.c_float)转换为int?

重新匹配{ }中包含的文本,其中文本可能包含{{var}

追溯(最近最后一次调用):文件C:\Users\Diplom/PycharmProject\Yolo01\Roboflow-4.py,第4行,在模块导入roboflow中

输出中带有南的亚麻神经网络

按顺序合并2个词典列表

修复mypy错误-赋值中的类型不兼容(表达式具有类型xxx,变量具有类型yyy)

如何请求使用Python将文件下载到带有登录名的门户网站?

在np数组上实现无重叠的二维滑动窗口

对象的`__call__`方法的setattr在Python中不起作用'

为什么NumPy的向量化计算在将向量存储为类属性时较慢?'

Pandas—在数据透视表中占总数的百分比

启动带有参数的Python NTFS会导致文件路径混乱

try 检索blob名称列表时出现错误填充错误""

找到相对于列表索引的当前最大值列表""

Python日志(log)模块如何在将消息发送到父日志(log)记录器之前向消息添加类实例变量

无法在Spyder上的Pandas中将本地CSV转换为数据帧

从列表中分离数据的最佳方式

无法在盐流道中获得柱子

如何获取给定列中包含特定值的行号?

无法使用请求模块从网页上抓取一些产品的名称