在Python中添加函数组合的语法糖是一个好主意吗?

19

前段时间我查看了Haskell文档,发现它的函数组合运算符非常好用。因此,我实现了这个小装饰器:

from functools import partial

class _compfunc(partial):
    def __lshift__(self, y):
        f = lambda *args, **kwargs: self.func(y(*args, **kwargs)) 
        return _compfunc(f)

    def __rshift__(self, y):
        f = lambda *args, **kwargs: y(self.func(*args, **kwargs)) 
        return _compfunc(f)

def composable(f):
    return _compfunc(f)

@composable    
def f1(x):
    return x * 2

@composable
def f2(x):
    return  x + 3

@composable
def f3(x):
    return (-1) * x

@composable
def f4(a):
    return a + [0]

print (f1 >> f2 >> f3)(3) #-9
print (f4 >> f1)([1, 2]) #[1, 2, 0, 1, 2, 0]
print (f4 << f1)([1, 2]) #[1, 2, 1, 2, 0]

问题: 如果没有语言支持,我们无法在内置函数或lambda中使用此语法:

((lambda x: x + 3) >> abs)(2)

问题是:它有用吗?值得在Python邮件列表上讨论吗?


...但仍然可以编写类似以下的代码: print (composable(lambda x: x + 3) >> composable(abs))(-4) - si14
对于数学函数,通常使用*表示函数的组合,使用+表示函数的求和,例如(f * g + h)(x) = f(g(x)) + h(x)。此外,还可以使用幂运算符:f**2(x) = f(f(x))等等... - nadapez
4个回答

8
在我看来,不是这样的。虽然我喜欢 Haskell,但它似乎并不适合 Python。你可以使用 compose(f1, f2, f3) 代替 (f1 >> f2 >> f3),这样就可以解决你的问题了——你可以使用任何可调用对象而不需要重载、装饰或更改核心(如果我没记错的话,有人已经至少提出过 functools.compose;但我现在找不到了)。
此外,语言定义现在已经被冻结,因此他们可能会拒绝那种改变——请参见 PEP 3003

3
@si14:我不了解Haskell,所以我也不太清楚>>和<<的含义(它是插入还是包装其他函数?)。 - nikow
1
@Nikow:我也不知道,但是>>和<<明确定义了“计算流程”:f1 >> f2 >> f3的意思是“将f1的结果放到f2中,然后将f2的结果放到f3中”。另一种选择是f1(f2(f3(x))),这样会有很多混乱的括号。 - si14
1
@si14:我理解其中的逻辑,但仍然认为这主要是习惯问题。至少有一种函数式语言过度使用括号;-) 对我来说,这听起来类似于(Java)人们抱怨len(a)不是面向对象的,应该是a.len()。 - nikow
我认为 |> 操作符不怎么易读。Python不能胜任所有任务。在某些方面,Haskell 和 F# 更加擅长。 - Hamish Grubijan
@si14请澄清您的意思,即f1 >> f2 >> f3lambda x:f3(f2(f1(x)))等效;毕竟,您是在说f1的结果将被馈送给f2等。看起来您在评论中打了一个错字。我想澄清这一点,因为如果世界上有人将“>>”解释为相反的方向,我想知道(在这种情况下,我将使与__rshift __相关的注释数量增加到我的代码中,我也使用它来表示组合)。 - max
显示剩余6条评论

6

在Python中,函数复合并不是一个非常常见的操作,特别是没有必要使用复合运算符。如果添加了这样的功能,我不确定是否喜欢Python选择使用<<>>,因为它们对我来说并不像你认为的那么显而易见。

我怀疑很多人会更喜欢一个名为compose的函数,其顺序也不成问题:compose(f, g)(x)意味着f(g(x)),与数学中的o和Haskell中的.相同。 Python试图避免使用标点符号,尤其是当英语单词可以代替时,特别是当这些特殊字符没有广泛认可的含义时。(有些例外情况,对于似乎太有用而无法放弃的东西,例如装饰器使用@(犹豫不决),函数参数使用*和**)

如果您选择将此发送到python-ideas,则如果您能找到一些标准库或流行的Python库中可以使用函数合成使代码更清晰,易于编写,易于维护或效率更高的实例,则可能会赢得更多人的支持。


1
你能否澄清一下为什么 >><< 不是显而易见的?我并不反对,只是想看看程序员之间是否有共识。 - max
@max,我不知道如何描述为什么某些事情“不明显”。我根本没有理由去看 <<>> 并说:“啊,它们应该用于这个!” - Mike Graham
@max 尝试解释:由于没有那种使用的传统,所以它不太明显。同样,由于在扩展打印语句(“打印尖括号”)中使用了>>,因此也不太明显。此外,将移位操作特殊函数的组合绑定到所有可调用对象不太明显:不清楚这会使哪些类型的对象失去资格。 - n611x007
@max:此外:明确优于隐式(f1 >> f2 << f3 >> f4)(3)有多明确? - n611x007
@naxa,这并不明显,但我无法确定其中隐含的意思。 - Mike Graham

6
你可以使用 reduce 来完成,但是调用顺序只能从左到右:
def f1(a):
    return a+1

def f2(a):
    return a+10

def f3(a):
    return a+100

def call(a,f):
    return f(a)


reduce(call, (f1, f2, f3), 5)
# 5 -> f1 -> f2 -> f3 -> 116
reduce(call, ((lambda x: x+3), abs), 2)
# 5

没错,但这不是中缀表达式(即 # 5 -> f1 -> f2 -> f3 -> 116)。 - alancalvitti

1

我对于Python的经验不足以对语言变更是否值得进行发表意见。但是我想描述一下当前语言可用的选项。

为了避免产生意外行为,函数组合应该理想地遵循标准数学(或Haskell)的运算顺序,即f ∘ g ∘ h应该意味着先应用h,然后g,最后f

如果你想在Python中使用现有的运算符,比如 <<,正如你提到的,你可能会遇到lambda和内置函数的问题。你可以通过定义反射版本__rlshift__来简化生活,除了__lshift__之外。这样,与composable对象相邻的lambda/内置函数就会被处理好。当你有两个相邻的lambda/内置函数时,你需要用composable显式地转换它们中的一个(只需一个),就像@si14建议的那样。请注意,我真正意思是__rlshift__,而不是__rshift__;事实上,我建议根本不要使用__rshift__,因为顺序的改变会让人感到困惑,尽管操作符形状提供了方向提示。

但是还有另一种方法,你可能想考虑一下。Ferdinand Jamitzky有一个很棒的配方,可以在Python中定义伪中缀运算符,甚至可以在内置函数上工作。有了这个,你可以写f |o| g来进行函数组合,看起来非常合理。


1
为了澄清,在引用的框架中,每个函数都必须被定义为“Infix”运算符,而不像Mathematica(和其他语言)那样,函数可以在不重新定义它们的情况下组合。 - alancalvitti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接