【问题标题】:Type annotation for Callable that takes **kwargs需要 **kwargs 的 Callable 的类型注释
【发布时间】:2020-08-17 12:39:20
【问题描述】:

有一个函数 (f) 使用一个函数签名 (g)第一组参数和任意数量的关键字参数**kwargs。有没有办法在 (f 中描述的 (g) 的类型签名中包含 **kwargs强>)?

例如:

from typing import Callable, Any
from functools import wraps
import math


def comparator(f: Callable[[Any, Any], bool]) -> Callable[[str], bool]:
    @wraps(f)
    def wrapper(input_string: str, **kwargs) -> bool:
        a, b, *_ = input_string.split(" ")
        return f(eval(a), eval(b), **kwargs)

    return wrapper


@comparator
def equal(a, b):
    return a == b


@comparator
def equal_within(a, b, rel_tol=1e-09, abs_tol=0.0):
    return math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)


# All following statements should print `True`
print(equal("1 1") == True)
print(equal("1 2") == False)
print(equal_within("5.0 4.99998", rel_tol=1e-5) == True)
print(equal_within("5.0 4.99998") == False)

函数comparatorwrapper 包装它的参数f,它将f 的输入作为字符串使用,对其进行解析并使用f 对其进行评估。在这种情况下,Pycharm 会发出警告,指出 return f(eval(a), eval(b), **kwargs) 使用意外参数 **kwargs 调用 f,这与预期的签名不匹配。

This post on Reddit 建议将Any... 添加到f 的类型签名中

  • f: Callable[[Any, Any, ...], bool]
  • f: Callable[[Any, Any, Any], bool]

前者导致 TypeError [1],而后者似乎具有误导性,因为f 接受至少 2 个参数,而不是正好 3 个。

另一种解决方法是将Callable args 定义与... 一样打开f: Callable[..., bool],但我想知道是否有更合适的解决方案。

  1. TypeError: Callable[[arg, ...], result]: each arg must be a type. Got Ellipsis.

【问题讨论】:

  • 基于这两个 github 问题 12,目前的 Callable 规范似乎没有涵盖这一点。
  • 我不小心将同一个问题链接了两次。预期的第二个问题是this

标签: python typing


【解决方案1】:

tl;dr:Protocol 可能是最接近已实现的功能,但仍不足以满足您的需求。详情请见this issue


完整答案:

我认为与您所要求的功能最接近的功能是 Protocol,它是在 Python 3.7 中引入的(并通过 typing_extensions 向后移植到 3.6)。它允许您定义描述类型行为的Protocol 子类,非常类似于其他语言中的“接口”或“特征”。对于函数,支持类似的语法:

from typing import Protocol
# from typing_extensions import Protocol  # if you're using Python 3.6

class MyFunction(Protocol):
    def __call__(self, a: Any, b: Any, **kwargs) -> bool: ...

def decorator(func: MyFunction):
    ...

@decorator  # this type-checks
def my_function(a, b, **kwargs) -> bool:
    return a == b

在这种情况下,任何具有匹配签名的函数都可以匹配MyFunction 类型。

但是,这不足以满足您的要求。为了使函数签名匹配,函数必须能够接受任意数量的关键字参数(即,具有**kwargs 参数)。到目前为止,仍然无法指定函数可以(可选地)采用任何关键字参数。 This GitHub issue 讨论了在当前限制下的一些可能(尽管冗长或复杂)的解决方案。


现在,我建议只使用Callable[..., bool] 作为f 的类型注释。不过,可以使用Protocol 来优化包装器的返回类型:

class ReturnFunc(Protocol):
    def __call__(self, s: str, **kwargs) -> bool: ...

def comparator(f: Callable[..., bool]) -> ReturnFunc:
    ....

这消除了equal_within("5.0 4.99998", rel_tol=1e-5) 处的“意外关键字参数”错误。

【讨论】:

【解决方案2】:

在 Python 3.10 中使用PEP 612,您可以尝试以下解决方案:

from typing import Callable, Any, ParamSpec, Concatenate
from functools import wraps

P = ParamSpec("P")


def comparator(f: Callable[Concatenate[Any, Any, P], bool]) -> Callable[Concatenate[str, P], bool]:
    @wraps(f)
    def wrapper(input_string: str, *args: P.args, **kwargs: P.kwargs) -> bool:
        a, b, *_ = input_string.split(" ")
        return f(eval(a), eval(b), *args, **kwargs)

    return wrapper

但是,您似乎无法摆脱 *args: P.args(您实际上不需要),因为 PEP 612 要求 P.argsP.kwargs 一起使用。如果您可以确保您的修饰函数(例如 equalequal_within)不使用额外的位置参数,则类型检查器应拒绝使用额外位置参数调用函数的任何尝试。

【讨论】:

  • ParamSpecKwargs 怎么样?这不是专门针对这个用例的
猜你喜欢
  • 2016-08-30
  • 2021-09-28
  • 2017-11-03
  • 2022-08-02
  • 2018-09-07
  • 2017-12-15
  • 1970-01-01
  • 2021-08-24
  • 1970-01-01
相关资源
最近更新 更多