3

我正在尝试稍微延迟评估,因此我更喜欢尽可能长时间地使用函数。

我有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(它应该约束向量中的每个单独元素)。我看到三个选项:

  1. 为向量上的函数创建一个新版本cap_if,比如说。vector_cap_if但是我需要为所有其他高阶函数这样做,这感觉不受欢迎。不过,这种方法的优势在于,我将来可以用函数替换这些函数的实现,例如,numpy获得巨大性能提升的函数。

  2. 实现将函数类型从“number -> number”“提升”到“<function from Data to number> 到 <function from Data to number>”,以及从“number -> number”到“<function from数据向量到数字> 到 <从数据向量到数字的函数>"。让我们称这些函数raise_to_data_functionraise_to_vector_function. 然后我可以定义basic_cap_if为单个数字的函数(而不是高阶函数);对于我需要的其他类似的辅助函数,我也这样做。然后我使用raise_to_data_function(basic_cap_if)代替cap_ifraise_to_vector_function(basic_cap_if)代替cap_if_vector。这种方法似乎更优雅,因为我只需要实现每个基本功能一次。但它失去了我上面描述的可能的性能提升,

  3. 我可以遵循 2 中的方法,但在需要时根据上下文自动应用raise_to_data_function函数raise_to_vector_function。大概我可以在__or__方法(函数应用程序)中实现这一点:如果它检测到一个函数被传递给simple_cap_if,它将检查被传递函数的签名,并将适当的raise_to函数应用于右侧。(例如,可以通过使具有不同子类的不同签名成员的函数来公开签名Function;或者通过在 中指定方法Function)。这似乎很 hacky,因为可能会发生很多隐式类型转换;但它确实减少了代码混乱。

我是否错过了更好的方法,和/或一些支持/反对这些方法的论点?

4

0 回答 0