我正在尝试稍微延迟评估,因此我更喜欢尽可能长时间地使用函数。
我有class Function
它定义了函数的组合和逐点算术:
from functools import reduce
def compose(*funcs):
'''
Compose a group of functions f1, f2, f3, ... into (f1(f2(f3(...))))
'''
result = reduce(lambda f, g: lambda *args, **kaargs: f(g(*args, **kaargs)), funcs))
return Function(result)
class Function:
'''
>>> f = Function(lambda x : x**2)
>>> g = Function(lambda x : x + 4)
>>> h = f/g
>>> h(6)
3.6
>>> (f + 1)(5)
26
>>> (2 * f)(3)
18
# >> means composition, but in the order opposite to the mathematical composition
>>> (f >> g)(6) # g(f(6))
40
# | means apply function: x | f is the same as f(x)
>>> 6 | f | g # g(f(6))
40
'''
# implicit type conversion from a non-callable arg to a function that returns arg
def __init__(self, arg):
if isinstance(arg, Function):
# would work without this special case, but I thought long chains
# of nested functions are best avoided for performance reasons (??)
self._func = arg._func
elif callable(arg):
self._func = arg
else:
self._func = lambda *args, **kwargs : arg
def __call__(self, *args, **kwargs):
return self._func(*args, **kwargs)
def __add__(lhs, rhs):
# implicit type conversions, to allow expressions like f + 1
lhs = Function(lhs)
rhs = Function(rhs)
new_f = lambda *args, **kwargs: lhs(*args, **kwargs) + rhs(*args, **kwargs)
return Function(new_f)
# same for __sub__, __mul__, __truediv__, and their reflected versions
# ...
# function composition
# similar to Haskell's ., but with reversed order
def __rshift__(lhs, rhs):
return compose(rhs, lhs)
def __rrshift__(rhs, lhs):
return compose(rhs, lhs)
# function application
# similar to Haskell's $, but with reversed order and left-associative
def __or__(lhs, rhs):
return rhs(lhs)
def __ror__(rhs, lhs):
return rhs(lhs)
最初,我的所有函数都具有相同的签名:它们将 的实例作为单个参数class Data
,并返回float
. 也就是说,我的实现Function
不依赖于这个签名。
然后我开始添加各种高阶函数。例如,我经常需要创建现有函数的有界版本,所以我写了一个函数cap_if
:
from operator import le
def cap_if(func, rhs):
'''
Input arguments:
func: function that determines if constraint is violated
rhs: Function(rhs) is the function to use if constraint is violated
Output:
function that
takes as an argument function f, and
returns a function with the same signature as f
>>> f = Function(lambda x : x * 2)
>>> 5 | (f | cap_if(le, 15))
15
>>> 10 | (f | cap_if(le, 15))
20
>>> 5 | (f | cap_if(le, lambda x : x ** 2))
25
>>> 1.5 | (f | cap_if(le, lambda x : x ** 2))
3.0
'''
def transformation(original_f):
def transformed_f(*args, **kwargs):
lhs_value = original_f(*args, **kwargs)
rhs_value = rhs(*args, **kwargs) if callable(rhs) else rhs
if func(lhs_value, rhs_value):
return rhs_value
else:
return lhs_value
return Function(transformed_f)
return Function(transformation)
这就是问题所在。我现在想介绍接受Data
实例“向量”并返回数字“向量”的函数。乍一看,我本可以保持现有框架不变。毕竟,如果我将向量实现为 ,numpy.array
那么向量将支持逐点算术,因此函数上的逐点算术将按预期工作,而无需对上面的代码进行任何更改。
但是上面的代码在高阶函数上中断,例如cap_if
(它应该约束向量中的每个单独元素)。我看到三个选项:
为向量上的函数创建一个新版本
cap_if
,比如说。vector_cap_if
但是我需要为所有其他高阶函数这样做,这感觉不受欢迎。不过,这种方法的优势在于,我将来可以用函数替换这些函数的实现,例如,numpy
获得巨大性能提升的函数。实现将函数类型从“number -> number”“提升”到“<function from Data to number> 到 <function from Data to number>”,以及从“number -> number”到“<function from数据向量到数字> 到 <从数据向量到数字的函数>"。让我们称这些函数
raise_to_data_function
和raise_to_vector_function
. 然后我可以定义basic_cap_if
为单个数字的函数(而不是高阶函数);对于我需要的其他类似的辅助函数,我也这样做。然后我使用raise_to_data_function(basic_cap_if)
代替cap_if
和raise_to_vector_function(basic_cap_if)
代替cap_if_vector
。这种方法似乎更优雅,因为我只需要实现每个基本功能一次。但它失去了我上面描述的可能的性能提升,我可以遵循 2 中的方法,但在需要时根据上下文自动应用
raise_to_data_function
函数raise_to_vector_function
。大概我可以在__or__
方法(函数应用程序)中实现这一点:如果它检测到一个函数被传递给simple_cap_if
,它将检查被传递函数的签名,并将适当的raise_to
函数应用于右侧。(例如,可以通过使具有不同子类的不同签名成员的函数来公开签名Function
;或者通过在 中指定方法Function
)。这似乎很 hacky,因为可能会发生很多隐式类型转换;但它确实减少了代码混乱。
我是否错过了更好的方法,和/或一些支持/反对这些方法的论点?