如何键入一个以可调用对象及其参数作为参数的函数 - 例如函数的数值积分?
Posted
技术标签:
【中文标题】如何键入一个以可调用对象及其参数作为参数的函数 - 例如函数的数值积分?【英文标题】:How to typehint a function which takes a callable and its args as parameters - e.g. a numerical integration of a function? 【发布时间】:2021-08-11 10:17:06 【问题描述】:问题
我有以下功能(基于scipy.integrate.quad
):
def simple_quad(func: Any, a: float, b: float, args: tuple = ()) -> float:
def strips(n: int):
for i in range(n):
x = a + (b - a) * i / n
yield func(x, *args) * 1 / n
return sum(strips(1000))
...它基本上在一系列值上评估func
,并使用固定宽度的条带来计算图形下的面积。可选地,参数可以通过args
元组传递给func
。
如您所见,我已经做了一些初始类型提示(实际上这是 scipy 的 .pyi 存根),但是我对 func 和 args 的类型如此松散感到不满意。我希望 mypy 保护我免受两件事的影响:
func
是一个可调用对象,它必须具有第一个位置参数 float
,并返回 float
,并且可以具有可选的 *args
即至少f(x:float, ...) -> float
我猜它也可以有 **kwargs(虽然不能有 required name-only params 或 x 以外的必需位置参数)
*args
到 func
的可选位置 必须与 splatted args 元组的内容相匹配
示例
def cubic(x: float, a: float, b: float, c: float, d: float) -> float:
"Function to evaluate a cubic polynomial with coefficients"
return a + b * x + c * x ** 2 + d * x ** 3
simple_quad(cubic, a=1, b=10, args=(1, 2, 3, 4)) # fine, passes args to a,b,c,d and int is compatible with float signature
simple_quad(cubic, a=1, b=10) # error from mypy as *args to `cubic` don't have default args so are all required
simple_quad(cubic, a=1, b=10, args=("a", "b", "c", "d")) # arg length correct but type mismatch since cubic expects floats
x_squared: Callable[[float], float] = lambda x: x * x
simple_quad(x_squared, a=1, b=10, args=()) # should be fine as x_squared doesn't take any positional args other than x
def problematic(x: float, *, y: float) -> float: ... # can't pass kwargs y via simple_quad, so can't be integrated
我尝试过的
对于func
,我尝试使用协议和泛型:
class OneDimensionalFunction(Protocol, Generic[T]): #double inheritance, although maybe I can get by with a metaclass for Generic
def __call__(self, x: float, *args: T) -> float: ...
...希望我能写出来
def simple_quad(func: OneDimensionalFunction[T], a: float, b: float, args: tuple[T] = ()) -> float:
simple_quad(cubic, 1, 10, 10, (1,2,3,4)) # infer type requirement of args based on signature of func
# or
simple_quad[float,float,float,float](cubic, ...) #pass in additional type info above and beyond the expected Callable[[x:float,...], float]
...我知道有很多问题,如果例如我想将 lambda 作为 func 传递,Protocol 也不能很好地与 Callable 配合使用。
我将此标记为 python 3.10,因为我认为新的 Parameter Specification Variables 可能会有所帮助,但我只看到过装饰器中使用的那些,所以我不确定如何在这里应用它们。让我知道你的想法
【问题讨论】:
@OlvinRoght 恐怕这是一个棘手的问题 这样的逻辑不属于类型提示。它应该是运行时验证。 @rdas 我不知道,这感觉像是一种很简单的方法,可以将不兼容的参数传递给函数。只是因为通过包装函数进行了额外的重定向,所以输入变得复杂,如果 func 是 functools.partial 我认为它会更容易(但它不能因为 scipy 也接受需要单独的 args 的 LowLevelCallables )。另外我认为Parameter Specification Variables 至少是为了解决这类问题而添加的;对装饰器的可调用参数的类型提示 *args。 类型提示不会阻止您自己在脚下开枪。在强类型语言中,解决方案是为args
定义一个类型并改用它。尝试使用类型提示做类似的事情 - 假设它是可能的 - 将是一个丑陋且无法维护的 IMO。
是什么让你认为它可以有 kwargs? github.com/scipy/scipy/blob/…
【参考方案1】:
对协议使用重载。这不是很好,但它可以让您尽可能接近真正的验证。
from typing import Protocol, Tuple, Any, overload, Union, Optional
class C1(Protocol):
def __call__(self, a: float, ) -> float: ...
class C2(Protocol):
def __call__(self, a: float, b: float, ) -> float: ...
class C3(Protocol):
def __call__(self, a: float, b: float, c: float) -> float: ...
ET = Tuple[()]
T1 = Tuple[float]
T2 = Tuple[float, float]
T3 = Tuple[float, float, float]
@overload
def quad(func: C1, a: float, b: float, args: Union[ET, T1] = ()) -> float: ...
@overload
def quad(func: C2, a: float, b: float, args: Union[ET, T2] = ()) -> float: ...
@overload
def quad(func: C3, a: float, b: float, args: Union[ET, T3] = ()) -> float: ...
def quad(func: Any, a: float, b: float, args: tuple = ()) -> float:
return 0
def cubic(a: float, b: float, c: float, *, s: Optional[str] = None) -> float:
return 0
quad(cubic, 0, 1)
【讨论】:
这确实提供了我所追求的功能,非常公平。当然,减少复制粘贴会很好——只是想知道你是否有机会查看 3.10 的 Parameter Specification Variables,因为它们似乎与这个问题有关,但我不太明白在他们周围......以上是关于如何键入一个以可调用对象及其参数作为参数的函数 - 例如函数的数值积分?的主要内容,如果未能解决你的问题,请参考以下文章