何时使用reduce()而不是sum()?

16

我最近开始学习函数式编程,在尝试计算班级测验平均成绩时,想出了以下示例:

我所想出的示例是:

scores = [90, 91, 92, 94, 95, 96, 97, 99, 100]

def add(num1, num2):
    '''returns the sum of the parameters'''
    return num1 + num2

import operator 

timeit reduce(add, scores) / len(scores)  #--> 1000000 loops, best of 3: 799 ns per loop

timeit sum(scores) / len(scores)  #--> 1000000 loops, best of 3: 207 ns per loop

timeit reduce(operator.add, scores) / len(scores) #--> 1000000 loops, best of 3: 485 ns per loop

从上面的例子来看,似乎使用高阶函数要慢近4倍。

那么我的问题是,什么时候使用高阶函数才是一个好的选择呢?因为显然在上面的例子中不是。


使用operator.add代替自定义函数怎么样?另外,有两点需要注意:1.sum在代码的可读性方面可能更好(在您的情况下是这样);2.Guido不喜欢reduce - fjarri
@repzero,您所说的“函数细节”是什么意思? - Dor-Ron
@fjarri使用operator.add添加了一个case,但sum仍然更快。我同意在大多数情况下sum更可读,但我想知道高阶函数何时可能会产生任何性能优势或其他类型的优势。 - Dor-Ron
4个回答

13
reduce()在需要对一系列数据执行任意操作时有用,但当您已经拥有一个高度优化的库函数,不仅可以在小型列表上胜过reduce(),而且在较大的列表上也可以极大地提高性能。

reduce()为您提供了创建任意折叠的灵活性,但这种灵活性是以一些性能开销为代价的,特别是在一个将大多数基本函数构造视为略微超出主流的语言中。

Python之所以“函数式”,是因为它具有一流的函数,但它并不是一个主要的函数式语言。它提供了丰富的迭代器供循环使用,并且具有各种语言功能,使得编写显式循环变得容易,但并没有围绕递归定义的列表操作(尽管它允许在有限程度内进行——缺少TCO会阻止我直接在Python中转述我的Erlang或Guile代码,但确实给了我灵活性来做像遵守类似接口的竞争方法的基准测试)。


6
抛开性能问题,我必须说:使用 sum() 没有任何问题,从风格上讲,你应该选择 sum() 而不是 reduce()reduce() 更通用,因此可以用于编写除求和之外的其他归约操作。而 sum() 是一种常见的归约操作,其名称和定义值得拥有。
如果您查看函数式编程语言,例如 Haskell 的 Data.List 或 Scheme 的 SRFI-1,您会发现它们拥有大型的通用实用程序函数库,用于处理序列。这些库的很多函数都可以根据其他函数编写;例如,Haskell 中的 map 函数可以根据 foldr(类似于 reduce())编写:
map :: (a -> b) -> [a] -> [b]
map f = foldr go []
  where f a bs = f a : bs

但是没有人认为foldr因此使map变得不必要或需要避免。相反,像foldrreduce()这样的更通用操作被视为构建更专业函数的基础模块,这些函数使程序更易于编写和理解。 reduce()sum()之间存在着相同的关系。reduce()是一个构建模块,当你没有像sum()这样的函数时,可以使用它。

5

reducesum 所做的事情非常不同。考虑这样一个问题:“我有一个嵌套的字典……”

d = {'foo': {'bar': {'baz': 'qux'}}}

我希望获取与这些键列表相关联的值:['foo','bar','baz']"。如果您是函数式编程类型的人,则可以使用reduce方法(也可能不需要):

>>> reduce(lambda subdict, k: subdict[k], ['foo', 'bar', 'baz'], d)
'qux'

注意,你不能使用sum来完成这个操作。只是因为求和是一个简单的例子,可以用来展示reduce的工作原理(因为你可以用括号把它写出来,并且大多数程序员都熟悉括号如何分组数学运算)。

虽然这是一个很好的例子,但自定义lambda在这里是不必要的,因为您基本上重新定义了自己的dict.get:您可以编写reduce(dict.get, ['foo', 'bar', 'baz'], d) - Stef
2
实际上,它应该是 reduce(dict.__getitem__, ["foo", "bar", "baz"], d)。这样,如果键不存在,你就可以恢复 KeyError 行为,而不是变成可能的 TypeError: NoneType is not subscriptable :)。 - mgilson

1

不要使用sum,永远不要使用。

但是当按自定义方法聚合时,reduce调用是一种可行的方式。

例如,product可以定义为:

product = lambda iterable: reduce(operator.mul, iterable)

同时,sum 已在C语言中实现。


如果你要添加的类型添加速度较慢,而你想避免进行一次添加呢? - Neil G
有一个重要的区别:sum总是使用起始值(“从左到右对可迭代对象的开始和项求和并返回总和”)。如果提供了初始化程序,reduce(operator.add, ...)只使用它(“如果可选的初始化程序存在,则将其放置在可迭代对象的项之前进行计算”)。 - moi
在Python>=3.8中,我们有math.prod来避免使用reduce(operator.mul, ) - Stef

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