Python中的函数组合运算符

时间:2019-01-12 17:43:29

标签: python python-decorators function-composition

this question中,我询问了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__之前已转换为字符串。

有没有办法解决这种转换?还是我误诊了问题,还有其他方法可行吗?

3 个答案:

答案 0 :(得分:3)

您没有想要的东西。 .表示不是二进制运算符,它是primary,只带有值操作数(.的左侧),并且标识符Identifiers是字符串,不是生成对值的引用的成熟表达式。

来自Attribute references section

  

属性引用是主引用,后跟句点和名称:

attributeref ::=  primary "." identifier
     

主要对象必须对支持属性引用的类型的对象求值,大多数对象都会这样做。然后要求该对象产生名称为标识符的属性。

因此,在编译时,Python将identifier解析为一个字符串值,而不是一个表达式(这是为运算符提供给运算符的结果)。 __getattribute__钩子(和任何其他attribute access hooks)仅需处理字符串。没有办法解决这个问题。动态属性访问函数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没有钩子,因为它们是根据对象标识而不是对象值进行操作的。)

答案 1 :(得分:0)

我实际上不愿意提供这个答案。但是您应该知道,在某些情况下,即使它是主要的,也可以使用点“ .”。此解决方案仅适用于可以从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

答案 2 :(得分:0)

您可以使用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更改为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