为采用 **kwargs 的 Callable 类型注解
Type annotation for Callable that takes **kwargs
有一个函数 (f) 使用函数签名 (g) 接受已知的第一组参数和任意数量的关键字参数 **kwargs
。有没有一种方法可以将 **kwargs
包含在 (g) 的类型签名中,如 ( f)?
例如:
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)
函数 comparator
用 wrapper
包装它的参数 f
,它将 f
的输入作为字符串使用,解析它并使用 [=14 计算它=].在这种情况下,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]
,但我想知道是否有更合适的解决方案。
TypeError: Callable[[arg, ...], result]: each arg must be a type. Got Ellipsis.
tl;dr:Protocol
may be the closest feature that's implemented, but it's still not sufficient for what you need. See this issue 了解详情。
完整答案:
我认为最接近您要求的功能是 Protocol
, which was introduced in Python 3.8 (and backported to older Pythons via typing_extensions
)。它允许您定义一个描述类型行为的 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
来优化包装器的 return 类型:
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)
处的“意外关键字参数”错误。
在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.args
和 P.kwargs
一起使用。如果你能确保你的修饰函数(例如 equal
和 equal_within
)不接受额外的位置参数,任何尝试调用具有额外位置参数的函数都应该被类型检查器拒绝。
有一个函数 (f) 使用函数签名 (g) 接受已知的第一组参数和任意数量的关键字参数 **kwargs
。有没有一种方法可以将 **kwargs
包含在 (g) 的类型签名中,如 ( f)?
例如:
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)
函数 comparator
用 wrapper
包装它的参数 f
,它将 f
的输入作为字符串使用,解析它并使用 [=14 计算它=].在这种情况下,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]
,但我想知道是否有更合适的解决方案。
TypeError: Callable[[arg, ...], result]: each arg must be a type. Got Ellipsis.
tl;dr:Protocol
may be the closest feature that's implemented, but it's still not sufficient for what you need. See this issue 了解详情。
完整答案:
我认为最接近您要求的功能是 Protocol
, which was introduced in Python 3.8 (and backported to older Pythons via typing_extensions
)。它允许您定义一个描述类型行为的 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
来优化包装器的 return 类型:
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)
处的“意外关键字参数”错误。
在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.args
和 P.kwargs
一起使用。如果你能确保你的修饰函数(例如 equal
和 equal_within
)不接受额外的位置参数,任何尝试调用具有额外位置参数的函数都应该被类型检查器拒绝。