函数式编程中的“ pythonic”等同于“ fold”函数是什么?


115

在Haskell中,实现以下目标的最惯用的方法是:

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

或等效的Ruby:

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

显然,Python提供了reduce与fold完全相同的功能,但实际上是如上所述的fold的实现,但是,有人告诉我,“ pythonic”编程方式是避免使用lambda术语和高阶函数,并尽可能使用列表理解。因此,有没有一种首选的方式来折叠列表或不是Python reduce函数的类似列表的结构,或者是reduce惯用的方式来实现此目的?


2
sum还不够好吗?
JBernardo 2012年

3
不知道这是否是您问题的一个很好的例子。使用可以轻松实现sum,您可能需要提供一些不同类型的示例。
jamylak'4

14
嘿JBernardo-对数字列表求和是一个相当简陋的例子,我对使用一些二进制运算和一个起始值而不是专门对整数求和的一个列表元素的累加的一般概念更感兴趣。
mistertim

1
@mistertim:sum()实际上为此提供了有限的功能。例如sum([[a], [b, c, d], [e, f]], [])返回[a, b, c, d, e, f]
乔尔·科内特

尽管使用列表进行操作的情况很好地演示了使用此技术要注意的事情- +列表在时间和内存上都是线性时间操作,使得整个调用是二次的。使用list(itertools.chain.from_iterable([a], [b,c,d],[e,f],[]])总体上是线性的-如果只需要遍历一次,则可以删除该调用list以使其在内存方面恒定。
lvc

Answers:


114

用Python方式对数组求和的方法是使用sum。为了其他目的,有时可以使用reduce(来自functools模块)和operator模块的某种组合,例如:

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

请注意,reduce实际上这是一个foldl用Haskell表示的。没有执行折叠的特殊语法,没有内置函数foldr,实际上reduce与非关联运算符一起使用被认为是不良样式。

使用高阶函数是相当Python的;它很好地利用了Python的原理,即一切都是对象,包括函数和类。没错,lambda被某些Pythonista所反对,但这主要是因为它们变得复杂时往往不太可读。


4
@JBernardo:您是说不是buildinins模块中的内容不是pythonic?
弗雷德·富

4
不,这很愚蠢。但是给我一个单一的原因,为什么您认为GvR在从内置函数中删除它时会讨厌这么多的reduce函数
JBernardo

6
@JBernardo:因为人们试图用它来玩太聪明的把戏。引用该博客文章中的话:“的适用性reduce()几乎仅限于关联运算符,在所有其他情况下,最好明确地写出累加循环。” 因此,它的使用受到限制,但是即使GvR显然也必须承认其有用性,才能将其保留在标准库中。
Fred Foo 2012年

13
@JBernardo,这是否意味着Haskell和Scheme中对fold的每次使用都同样不好?这只是一种不同的编程风格,忽略它并把手指放在耳朵里,然后说不清楚还不是。像大多数其他风格不同的东西一样,需要练习才能习惯它。这样做的想法是将事物归为一般类别,以便更轻松地进行程序推理。“哦,我想这样做,嗯,看起来像是折叠”(或地图,展开或展开然后是折叠)
Wes 2012年

3
Python中的Lambda不能包含多个表达式。即使努力,也不能使其复杂。因此,不喜欢Pythonista的人可能只是不习惯,因此不喜欢函数式编程风格。
golem 2015年

16

哈斯克尔

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

蟒蛇

reduce(lambda a,b: a+b, [1,2,3,4,5], 0)

显然,这是一个说明问题的简单例子。在Python中,您只需要这样做sum([1,2,3,4,5]),甚至Haskell纯粹主义者通常也会更喜欢sum [1,2,3,4,5]

对于没有明显便利函数的非平凡场景,惯用的pythonic方法是显式写出for循环并使用可变变量分配,而不是使用reduceor fold

那根本不是功能样式,而是“ pythonic”方式。Python并非为功能纯正者设计。了解Python如何支持流控制的异常,以了解非功能性惯用python的情况。


12
折叠不仅对功能“纯粹主义者”有用。它们是通用抽象。递归问题普遍存在于计算中。折叠功能提供了一种删除样板的方法,并提供了一种以递归本身不支持递归的语言使递归解决方案安全的方法。这是非常实际的事情。GvR在这方面的偏见是不幸的。
itsbruce

12

在Python 3中,reduce已被删除:版本说明。不过,您可以使用functools模块

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

另一方面,文档表达了对for-loop而不是的偏好reduce,因此:

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

7
reduce没有从Python 3标准库中删除。reduce移至functools您显示的模块。
粘土

@clay,我只是从Guido的发行说明中选了那句话,但您可能是对的:)
Kyr

5

您也可以重新发明轮子:

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

7
因为Python缺少尾递归,所以这会在更长的列表上中断并且很浪费。此外,这并不是真正意义上的“折叠”功能,而仅仅是左折,即与foldl,那就是究竟什么reduce是减少的函数签名已经提供了(注意是reduce(function, sequence[, initial]) -> value-它也包括给予的初始值的功能累加器)。
cemper93

5

并不是真正回答这个问题,而是折叠和折叠的一线:

a = [8,3,4]

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

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

2
我觉得这是写你的foldr相似更好的方法:reduce(lambda y, x: x**y, reversed(a))。现在,它具有更自然的用法,可与迭代器一起使用,并且消耗更少的内存。
Mateen Ulhaq '18 -10-20

5

开始Python 3.8并引入赋值表达式(PEP 572):=运算符),这使您可以为表达式的结果命名,我们可以使用列表推导来复制其他语言称为fold / foldleft / reduce的操作:

给定一个列表,一个约简函数和一个累加器:

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

我们可以折叠itemsf在为了获得所得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

3

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

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

这将比reduce更快,而PyPy之类的东西可以优化循环。

顺便说一句,总和的情况应该用sum函数来解决


4
对于像这样的示例,这不会被认为是pythonic。
jamylak 2012年

7
Python循环异常缓慢。使用(或滥用)reduce是优化Python程序的常用方法。
Fred Foo 2012年

1
@larsmans请,不要说reduce比一个简单的循环要快...每次迭代总是有一个函数调用开销。同样,Pypy可以优化C速度的循环
JBernardo '04 -4-28

1
@JBernardo:是的,这就是我的要求。我只是product按照您的风格针对我的版本进行了配置,它的速度更快(不过略有提高)。
弗雷德·富

1
@JBernardo假设使用一个内置函数(如operator.add)作为reduce的参数:额外的调用是C调用(比Python调用便宜得多),并且节省了调度和解释几个字节码指令的过程,这很容易导致数十个函数调用。

1

我相信这个问题的一些回答者错过了该fold功能作为抽象工具的广泛含义。是的,sum可以对整数列表执行相同的操作,但这是不重要的情况。fold更通用。当您具有一系列形状各异的数据结构并想要清晰地表达聚合时,此功能很有用。因此,不必for每次都用一个聚合变量建立一个循环并手动重新计算它,fold函数(或reduce似乎对应的Python版本)允许程序员通过简单地提供以下内容来更清楚地表达聚合的意图:两件事情:

  • 聚合的默认起始值​​或“种子”值。
  • 该函数采用聚合的当前值(以“种子”开头)和列表中的下一个元素,并返回下一个聚合值。

嗨,rq_!我认为,如果您举了一个fold用Python很难做到的简单例子,然后在Python中做到这一点的非平凡示例,那么您的答案将会得到改善,并增加很多fold:-)
Scott Skiles

0

我参加聚会可能已经很晚了,但是我们可以foldr使用简单的lambda演算和curried函数创建自定义项。这是我在python中实现的foldr。

def foldr(func):
    def accumulator(acc):
        def listFunc(l):
            if l:
                x = l[0]
                xs = l[1:]
                return func(x)(foldr(func)(acc)(xs))
            else:
                return acc
        return listFunc
    return accumulator  


def curried_add(x):
    def inner(y):
        return x + y
    return inner

def curried_mult(x):
    def inner(y):
        return x * y
    return inner

print foldr(curried_add)(0)(range(1, 6))
print foldr(curried_mult)(1)(range(1, 6))

即使实现是递归的(可能很慢),它也会分别打印值15120

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.