Python 中的函数组合运算符

A function composition operator in Python

I asked about a function composition operator in Python. @Philip Tzou 中提供了以下代码,它完成了工作。

import functools

class Composable:

    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)

    def __matmul__(self, other):
        return lambda *args, **kw: self.func(other.func(*args, **kw))

    def __call__(self, *args, **kw):
        return self.func(*args, **kw)

我添加了以下功能。

def __mul__(self, other):
    return lambda *args, **kw: self.func(other.func(*args, **kw))

def __gt__(self, other):
    return lambda *args, **kw: self.func(other.func(*args, **kw))

通过这些添加,可以使用 @*> 作为运算符来组合函数。例如,可以写 print((add1 @ add2)(5), (add1 * add2)(5), (add1 > add2)(5)) 并得到 # 8 8 8。 (PyCharm 抱怨布尔值不能为 (add1 > add2)(5) 调用。但它仍然 运行。)

不过,一直以来,我都想使用 . 作为函数组合运算符。所以我添加了

def __getattribute__(self, other):
    return lambda *args, **kw: self.func(other.func(*args, **kw))

(请注意,这会弄脏 update_wrapper,为了这个问题可以将其删除。)

当我 运行 print((add1 . add2)(5)) 时,我在 运行 时收到此错误:AttributeError: 'str' object has no attribute 'func'。事实证明(显然)__getattribute__ 的参数在传递给 __getattribute__ 之前被转换为字符串。

是否有解决该转换的方法?还是我误诊了问题,其他一些方法会起作用吗?

你得不到你想要的。 . 表示法 不是二元运算符 ,它是 primary, with only the value operand (the left-hand side of the .), and an identifier. Identifiers 字符串,而不是生成值引用的完整表达式。

来自Attribute references section

An attribute reference is a primary followed by a period and a name:

attributeref ::=  primary "." identifier

The primary must evaluate to an object of a type that supports attribute references, which most objects do. This object is then asked to produce the attribute whose name is the identifier.

因此在编译时,Python 将 identifier 解析为字符串值,而不是表达式(这是运算符的操作数)。 __getattribute__ 钩子(以及任何其他 attribute access hooks) only has to deal with strings. There is no way around this; the dynamic attribute access function getattr() 钩子严格强制 name 必须是字符串:

>>> getattr(object(), 42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: getattr(): attribute name must be string

如果你想使用语法来组合两个对象,你只能使用二元运算符,所以表达式需要两个操作数,并且只有那些有钩子的(布尔andor 运算符没有钩子,因为它们延迟计算,isis not 没有钩子,因为它们对对象标识而不是对象值进行操作)。

这个答案我其实是不愿意的。但是你应该知道在某些情况下你可以使用点“.”符号,即使它是主要的。此解决方案仅适用于可从 globals():

访问的函数
import functools

class Composable:

    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)

    def __getattr__(self, othername):
        other = globals()[othername]
        return lambda *args, **kw: self.func(other.func(*args, **kw))

    def __call__(self, *args, **kw):
        return self.func(*args, **kw)

测试:

@Composable
def add1(x):
    return x + 1

@Composable
def add2(x):
    return x + 2

print((add1.add2)(5))
# 8

您可以使用 inspect 模块绕过在全局范围内专门定义可组合函数的限制。请注意,这与您所能获得的 Pythonic 差不多,使用 inspect 将使您的代码更难跟踪和调试。这个想法是使用 inspect.stack() 从调用上下文中获取命名空间,并在那里查找变量名。

import functools
import inspect

class Composable:

    def __init__(self, func):
        self._func = func
        functools.update_wrapper(self, func)

    def __getattr__(self, othername):
        stack = inspect.stack()[1][0]
        other = stack.f_locals[othername]
        return Composable(lambda *args, **kw: self._func(other._func(*args, **kw)))

    def __call__(self, *args, **kw):
        return self._func(*args, **kw)

请注意,我将 func 更改为 _func 到 half-prevent 冲突,如果您使用实际命名为 func 的函数进行组合。此外,我将您的 lambda 包装在 Composable(...) 中,以便它本身可以组合。

证明它在全局范围之外工作:

def main():
    @Composable
    def add1(x):
        return x + 1

    @Composable
    def add2(x):
        return x + 2

    print((add1.add2)(5))

main()
# 8

这为您提供了能够将函数作为参数传递的隐含好处,而无需担心将变量名称解析为全局范围内的实际函数名称。示例:

@Composable
def inc(x):
    return x + 1

def repeat(func, count):
    result = func
    for i in range(count-1):
        result = result.func
    return result
    
print(repeat(inc, 6)(5))
# 11