考虑下面的装饰器,它将任何二进制操作符扩展为多个参数。
from typing import Callable, TypeVar
from functools import reduce, wraps
T = TypeVar('T')
def extend(binop: Callable[[T, T], T]):
""" Extend a binary operator to multiple arguments """
@wraps(binop)
def extended(*args: T) -> T:
if not args:
raise TypeError("At least one argument must be given")
return reduce(binop, args)
return extended
然后,它可以如下使用。
@extend
def fadd(x: float, y: float) -> float:
""" Add float numbers """
return x + y
@extend
def imul(x: int, y: int) -> int:
""" Multiply integers """
return x*y
这样就可以创建 imul
和 fadd
函数,分别对其输入参数进行乘法和加法。
函数 imul
和 fadd
将会有正确的docstrings(由于 @wraps
decorator),但它们的签名和类型注释是不正确的。 例如
>>> help(fadd)
Gives
fadd(x: float, y: float) -> float
Add float numbers
另外
>>> fadd.__annotations__
{'x': <class 'float'>, 'y': <class 'float'>, 'return': <class 'float'>}
这是不正确的。
正确的实现装饰器的方法是什么,才能得出正确的函数签名?
我莫名其妙地认为,如果我去掉 @wraps
行的类型提示和签名将是正确的。 但即使是这样,也并非如此。 如果没有 @wraps
>>> help(fadd)
给予
extended(*args: ~T) -> ~T
(即,通用类型 T
没有被替换为 float
).
我发现了一个稍微有点黑的解决方案,使用 inspect
def extend(binop: Callable[[T, T], T]):
""" Extend a binary operator to multiple arguments """
@wraps(binop)
def extended(*args: T) -> T:
if not args:
raise TypeError("At least one argument must be given")
return reduce(binop, args)
sig = inspect.signature(extended)
sig = sig.replace(
parameters=[inspect.Parameter('args', inspect.Parameter.VAR_POSITIONAL,
annotation=sig.return_annotation)]
)
extended.__signature__ = sig
return extended
它并不像我所期望的那样优雅,尤其是它在很大程度上取决于这样一个事实,即 binop
返回值的类型必须和它的参数相同(否则注释会出错)。
然而,我很乐意知道更好的解决方案。