“pythonic”中的“fold”函数等效于函数式编程中的哪个函数?

176

在 Haskell 中,实现类似以下代码的最惯用方式是什么:

foldl (+) 0 [1,2,3,4,5]
--> 15

或者在Ruby中等价于:

[1,2,3,4,5].inject(0) {|m,x| m + x}
#> 15

显然,Python提供了reduce函数,就像上面的折叠实现一样,但是我被告知,“pythonic”编程的方式是避免使用lambda表达式和高阶函数,而尽可能使用列表推导式。因此,在Python中折叠一个列表或类似结构的首选方法是什么?是否存在不使用reduce函数的成语化方式实现这个操作?


3
“sum” 不够好吗? - JBernardo
3
不确定这是否是你问题的好例子。使用“sum”很容易实现,你可能需要提供一些不同类型的例子。 - jamylak
22
嘿,JBernardo - 对一列数字求和只是一个相当简单的例子,我更感兴趣于使用某些二元运算符和起始值来累积列表元素的一般思想,而不仅仅局限于对整数求和。 - mistertim
1
@mistertim:实际上,sum() 在这方面提供了有限的功能。例如,sum([[a], [b, c, d], [e, f]], []) 返回 [a, b, c, d, e, f] - Joel Cornett
虽然使用列表的情况是展示此技术需要注意的事项的好例子 - 列表上的 + 操作在时间和内存上都是线性的,使整个调用成为二次方程。使用 list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[])) 总体上是线性的 - 如果您只需要对其进行一次迭代,则可以删除对 list 的调用以使其在内存方面保持恒定。 - lvc
需要注意的是,foldLeft通常与reduce不同...您只能在monoid上进行reduce。foldLeft是一件更加普遍的事情。 - dividebyzero
8个回答

158

在Python中对数组求和的惯用方式是使用sum函数。对于其他用途,有时可以使用reduce函数(位于functools模块)与operator模块的某些组合,例如:

def product(xs):
    return reduce(operator.mul, xs, 1)

需要注意的是,在 Haskell 中,reduce 本质上是一个 foldl。没有特殊的语法来执行 folds,也没有内置的 foldr,实际上使用非关联操作符与 reduce 被认为是不好的风格。

使用高阶函数在 Python 中很常见,它很好地利用了 Python 的原则:一切皆对象,包括函数和类。你是对的,一些 Python 程序员不喜欢使用 Lambda 表达式,但这主要是因为当它们变得复杂时不易阅读。


4
你是在说除了内置模块之外的任何东西都不符合 Python 风格吗? - Fred Foo
4
不,那么说是很愚蠢的。但请给我一个理由,为什么你认为GvR为什么会如此讨厌reduce函数,以至于要将其从内置函数中删除? - JBernardo
7
由于人们试图用巧妙的方式玩弄它,所以reduce()的适用范围很大程度上仅限于可结合的操作符,在其他情况下最好明确地编写累加循环。因此,它的使用受到限制,但即使GvR显然不得不承认它足够有用,以便将其保留在标准库中。 - Fred Foo
22
@JBernardo,这是否意味着Haskell和Scheme中每个折叠使用都同样糟糕?这只是一种不同的编程风格,忽视它并捂住耳朵说它不清楚并不能改变事实。就像大多数不同风格的东西一样,需要练习才能习惯它。其想法是将事物归为通用类别,以便更容易推理程序。"哦,我想做这个,嗯,看起来像一个折叠"(或地图,或展开,或展开再对其进行折叠) - Wes
4
在Python中,Lambda不能包含多个表达式。即使努力尝试,也无法使其复杂化。因此,不喜欢Lambda的Python程序员可能只是不习惯函数式编程风格,所以才会不喜欢它们。 - golem
显示剩余4条评论

79

Python 3.8开始,并引入了赋值表达式(PEP 572):=运算符),使得我们可以给一个表达式命名结果,我们可以使用列表推导来模拟其他语言中称为折叠/折叠左侧/减少操作的操作:

给定一个列表、一个缩减函数和一个累加器:

items = [1, 2, 3, 4, 5]
f = lambda acc, x: acc * x
accumulator = 1

我们可以使用 f 折叠 items 以获得结果为 accumulation

[accumulator := f(accumulator, x) for x in items]
# accumulator = 120

或者用更简明的形式表达:

acc = 1; [acc := acc * x for x in [1, 2, 3, 4, 5]]
# acc = 120
请注意,实际上这也是一个“scanleft”操作,因为列表推导式的结果表示每个步骤中累积状态。
acc = 1
scanned = [acc := acc * x for x in [1, 2, 3, 4, 5]]
# scanned = [1, 2, 6, 24, 120]
# acc = 120

11
这应该是一个被接受的答案,而不是建议使用sum函数。 - dark_ruby
2
好的,这是实际答案。我很难过这不是最佳答案,而且许多其他页面都说Pythonic模拟reduce()函数就是reduce()函数。 为了便于搜索:Pythonic模拟reduce()函数可以使用:=将累加器引入列表推导式中。 请将“已接受的答案”更改为此答案。 - Roman Kogan
4
这并不是真正的折叠操作,它是一个“scanleft”操作,会在完成后丢弃临时列表。对于较长的列表,这种差异可能很重要。 - D0SBoots
1
为什么这很重要:
timeit.Timer("acc=0\nfor i in range(10000):acc+=i").repeat(5, 100) [0.036283817142248154, 0.032444920390844345, 0.03235280327498913, 0.032462552189826965, 0.03250854276120663]
timeit.Timer("acc=0\n[acc:=acc+i for i in range(10000)]").repeat(5, 100) [0.04360721819102764, 0.03987771272659302, 0.03988228552043438, 0.03988049365580082, 0.039858657866716385]
- D0SBoots
1
在记忆使用方面的差异比时间上的差异更重要。往往fold是在无法保留在内存中的可迭代对象上执行的。 - Dan Ganea

24

Haskell

foldl (+) 0 [1,2,3,4,5]

显然,这只是一个简单的例子,用于说明一点。在 Python 中,您只需使用 sum([1,2,3,4,5]),即使 Haskell 纯粹主义者通常也更喜欢使用 sum [1,2,3,4,5]

对于非平凡的情况,当没有明显的便捷函数时,惯用的 Python 风格是显式地编写 for 循环,并使用可变变量赋值,而不是使用 reducefold

这不是完全的函数式风格,但这是“Pythonic”的方法。Python 不是为函数纯粹主义者设计的。请查看 Python 如何偏爱异常来控制流程,以了解 Python 的非功能性惯用法。


15
折叠不仅对于功能主义者有用,它们是通用的抽象概念。递归问题在计算中非常普遍。折叠提供了一种消除样板代码的方式,并且在那些不支持本地递归的语言中,使递归解决方案更加安全。因此,这是一件非常实际的事情。 GvR在这个领域的偏见是不幸的。 - itsbruce
6
对我来说,JavaScript 拥有比 Python 更清晰、更实用的 lambda 表达式和高阶函数这一点真是非常奇怪。这让我感到非常沮丧,因为 Python 在其他方面都是一种设计得很好、很具吸引力的语言。 - iono
我同意其中的奇怪部分,但不同意“否则设计良好......”前半句。我使用这种语言作为主要语言已经四年了,之前还有八年使用它作为重要辅助语言。至少现在有些改进,例如类型提示、海象运算符和字典推导式等功能。 - WestCoastProjects

15
在Python 3中,reduce已被移除:发行说明。然而,你可以使用functools模块代替。
import operator, functools
def product(xs):
    return functools.reduce(operator.mul, xs, 1)

另一方面,文档表达了对 for 循环的偏好而非 reduce,因此:

def product(xs):
    result = 1
    for i in xs:
        result *= i
    return result

11
reduce没有从 Python 3 标准库中移除,正如你所示,reduce已经被移动到了 functools 模块中。 - clay
1
@clay,我只是从Guido的发布说明中引用了这个短语,但你可能是对的 :) - Kyr
我很高兴Guido大部分离开了。他声称不喜欢函数式编程,甚至想要摆脱_lambda_。 - WestCoastProjects

9

虽然不是问题的答案,但是foldl和foldr的一行代码:

a = [8,3,4]

## Foldl
reduce(lambda x,y: x**y, a)
#68719476736

## Foldr
reduce(lambda x,y: y**x, a[::-1])
#14134776518227074636666380005943348126619871175004951664972849610340958208L

10
我认为这是写你的foldr的更好方法:reduce(lambda y, x: x**y, reversed(a))。它现在有更自然的使用方式,适用于迭代器,并且占用的内存更少。 - Mateen Ulhaq

8

您也可以重新发明轮子:

def fold(f, l, a):
    """
    f: the function to apply
    l: the list to fold
    a: the accumulator, who is also the 'zero' on the first call
    """ 
    return a if(len(l) == 0) else fold(f, l[1:], f(a, l[0]))

print "Sum:", fold(lambda x, y : x+y, [1,2,3,4,5], 0)

print "Any:", fold(lambda x, y : x or y, [False, True, False], False)

print "All:", fold(lambda x, y : x and y, [False, True, False], True)

# Prove that result can be of a different type of the list's elements
print "Count(x==True):", 
print fold(lambda x, y : x+1 if(y) else x, [False, True, True], 0)

你在递归情况下交换了f的参数。 - KayEss
11
由于Python不支持尾递归,这段代码在较长的列表上会出现错误并且效率低下。此外,这并不是真正的“fold”函数,而仅仅是左折叠,即foldl,与 reduce 函数提供的功能完全相同(请注意,reduce 的函数签名为 reduce(function, sequence[, initial]) -> value,它也包括了给累加器提供初始值的功能)。 - cemper93

2
我相信这个问题的一些回答者忽略了fold函数作为一个抽象工具的更广泛含义。是的,sum可以对整数列表执行相同的操作,但这只是一个微不足道的情况。fold更加通用。当您有一系列形状各异的数据结构并希望清晰地表达聚合时,它非常有用。因此,与其建立一个带有聚合变量和每次手动重新计算的for循环,使用fold函数(或Python版本的reduce)允许程序员通过提供两个东西来更明确地表达聚合意图:起始值或“种子”值以进行聚合,以及一个函数,该函数接受聚合的当前值(从“种子”开始)和列表中的下一个元素,并返回下一个聚合值。

1
嗨rq_!我认为如果您提供一个在Python中难以干净地完成的fold的非平凡示例,然后在Python中“fold”,那么您的答案将得到改进并增加很多。 - Scott Skiles

0
这个与 IT 相关的内容的翻译是:

解决这个(减少)问题的实际答案是:只需使用循环!

initial_value = 0
for x in the_list:
    initial_value += x #or any function.

这样做比减少操作更快,例如PyPy可以优化这样的循环。

顺便说一下,求和问题应该使用 sum函数来解决。


8
对于这个例子来说,这种写法不符合 Python 风格。 - jamylak
9
Python 循环的速度通常较慢。使用(或滥用)reduce 是优化 Python 程序的常见方式。 - Fred Foo
2
@larsmans 请不要说reduce比简单循环更快... 它始终会有每次迭代的函数调用开销。 另外,再次提醒,Pypy可以将循环优化到C速度。 - JBernardo
2
@JBernardo:是的,这就是我所声称的。我刚刚对比了我的product版本和你的风格,它更快(虽然略微)。 - Fred Foo
2
假设将内置函数(例如 operator.add)作为参数传递给 reduce 函数:多出来的一次函数调用是一次 C 语言调用(比 Python 调用要快得多),这样可以避免分派和解释几个字节码指令,因此可以轻松地节省数十次函数调用。 - user395760
显示剩余3条评论

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