函数式编程HOWTO¶
作者: | Kuchling |
---|---|
发布: | 0.32 |
在本文档中,我们将介绍Python适用于函数式风格编程的特性。在介绍函数式编程概念之后,我们将看看诸如迭代器和生成器之类的语言特性以及相关的库模块,例如itertools
和functools
。
简介¶
本节介绍函数式编程的基本概念;如果你只是想了解Python语言特性,请跳至下一部分迭代器。
编程语言都支持几种不同的方式来分解问题︰
- 大多数编程语言是过程性的:程序是指令的列表,告诉计算机如何处理程序的输入。C、Pascal甚至Unix shell都是过程语言。
- 在声明性语言中,你编写一个描述要解决的问题的规范,而语言的实现计算出如何有效地执行计算。SQL是你最有可能熟悉的声明性语言; SQL查询描述要检索的数据集,SQL引擎决定是扫描表还是使用索引,哪些子语句应首先执行等。
- 面向对象程序操作对象的容器。对象具有内部状态以及以某种方式查询或修改此内部状态的支撑方法。Smalltalk和Java是面向对象的语言。C++和Python是支持面向对象编程的语言,但不强制使用面向对象的特性。
- 函数式编程将问题分解为一组函数。理想情况下,函数只接受输入并产生输出,并且没有影响给定输入产生的输出的任何内部状态。众所周知的函数式语言包括ML家族(标准ML,OCaml和其他变体)和Haskell。
一些计算机语言的设计者选择强调一种特定的编程方法。这通常使得使用不同方法编写程序变得困难。其他语言是支持几种不同方法的多范式语言。Lisp、C++和Python是多范式的;你可以编写程序或库,在所有这些语言中这些程序或库大部分是过程性的、面向对象的或函数式的。在大型程序中,可能使用不同的方法编写不同的片段; 例如,GUI可以是面向对象的,而处理逻辑是过程的或函数式的。
在函数式程序中,输入流过一组函数。每个函数对其输入进行操作并产生一些输出。函数式风格防止了函数的副作用,如修改内部状态或在函数返回值中不可见的其他更改。完全没有副作用的函数称为纯粹函数式。避免副作用意味着不使用随着程序运行而更新的数据结构;每个函数的输出必须只取决于它的输入。
一些语言对纯度非常严格,甚至没有赋值语句,例如a=3
或c = a + b
,但很难避免所有副作用。例如,打印到屏幕或写入磁盘文件就是副作用。例如,在Python中,对print()
或time.sleep()
函数的调用都不返回有用的值;调用它们就是要它们的副作用发送一些文本到屏幕或暂停执行一秒钟。
以函数式样式编写的Python程序通常不会走向避开所有I/O或所有赋值这样的极端;相反,它们将提供一个函数式外观的接口,但内部使用非函数式特性。例如,函数的实现仍将使用赋值到局部变量,但不会修改全局变量或具有其他副作用。
函数式编程可以被视为与面向对象编程相反。对象是包含一些内部状态的封装,以及一个允许你修改此状态的方法调用的容器,程序由正确的状态变化集合组成。函数式编程希望尽可能避免状态变化,并且在函数之间使用数据流。在Python中,你可以通过编写接收和返回表示应用程序中对象的实例的函数来组合这两种方法(电子邮件,事务等)。
函数式设计可能看起来像一个奇怪的约束。为什么要避免对象和副作用?函数式风格有理论和实践上的优点:
- 形式上的可证明性
- 模块化。
- 可组合性。
- 易于调试和测试。
形式上的可证明性¶
理论上的好处是,更容易构建数学证明证明函数式程序是正确。
很长一段时间里,研究人员一直对找到数学证明证明程序正确的方法感兴趣。这与从巨大的输入上测试程序并得出结论其输出通常是正确的,或者读取程序的源代码并得出结论代码看起来正确不同;其目标是一个严格的证明,程序对所有可能的输入产生正确的结果。
用于证明程序正确的技术是记下不变量,输入数据的属性和程序变量的属性总是为真。对于每行代码,你将显示如果不变量X和Y在这行之前执行,则稍微不同的不变量X'和Y'在这行执行之后为真。这将持续直到程序的结尾,此时不变量应该匹配程序输出上的所需条件。
函数式编程避免了赋值,因为赋值很难用这种技术处理;赋值可能打破在赋值之前为真的不变量,而不产生可以向前传播的任何新的不变量。
不幸的是,验证程序正确是很大程度上不切实际,与Python软件无关。即使是微不足道的程序也需要几页长的证据;一个中等复杂程序的正确性的证明将是巨大的,你每天使用的程序(Python解释器,你的XML解析器,你的Web浏览器)很少或没有一个可以证明是正确的。即使你记下或产生了一份证明,也会有验证证据的问题;也许有一个错误,你错误地认为你已经证明程序正确。
模块性¶
函数式编程的一个更实际的好处是它迫使你把你的问题分解成小块。因此,程序更模块化。比起执行复杂变换的大函数,指定和编写一个小的函数更容易。小功能也更容易阅读和检查错误。
易于调试和测试¶
测试和调试函数式风格的程序更容易。
调试简单是因为函数通常很小并且明确指定。当程序不工作时,每个函数都是一个接口点,你可以在其中检查数据是否正确。你可以查看中间输入和输出,以快速隔离导致错误的函数。
测试更容易,因为每个函数都是单元测试的潜在对象。函数不依赖于在运行测试之前需要复制的系统状态;而只需要合成正确的输入,然后检查输出是否符合预期。
可组合性¶
当你在一个函数式程序上工作时,你将编写一些具有不同输入和输出的函数。这些函数中的一些将不可避免地专用于特定应用,但是其他函数将在各种各样的程序中有用。例如,获取目录路径并返回目录中的所有XML文件的函数或者获取文件名并返回其内容的函数可以应用于许多不同的情况。
随着时间的推移,你将形成一个个人的公用库。通常,你将通过在新配置中排列现有函数并编写专用于当前任务的几个函数来组合新程序。
迭代器¶
我将首先看一个Python语言特性,这是编写函数式程序的重要基础:迭代器。
迭代器是表示数据流的对象;此对象一次返回一个元素的数据。Python迭代器必须支持一个称为__next__()
的方法,该方法不需要参数,并且总是返回流的下一个元素。如果流中没有更多元素,__next__()
必须引发StopIteration
异常。然而迭代器不必是有限的;编写一个产生无限数据流的迭代器是完全合理的。
内置的iter()
函数接受一个任意对象,并尝试返回一个迭代器,返回对象的内容或元素,如果对象不支持迭代,则引发TypeError
。Python的几种内置数据类型支持迭代,最常见的是列表和字典。如果你可以为其获取迭代器,则对象称为可迭代对象。
你可以手动实验迭代接口:
>>> L = [1,2,3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__() # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>>
Python在几个不同的上下文中期望可迭代对象,最重要的是for
语句。在for X in Y
语句中,Y必须是一个迭代器或者iter()
可以为其创建一个迭代器的某个对象。这两个语句是等效的:
for i in iter(obj):
print(i)
for i in obj:
print(i)
迭代器可以通过使用list()
或tuple()
构造函数转成列表或元组:
>>> L = [1,2,3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)
序列分拆也支持迭代器:如果你知道一个迭代器将返回N个元素,你可以将它们分拆成一个N元组:
>>> L = [1,2,3]
>>> iterator = iter(L)
>>> a,b,c = iterator
>>> a,b,c
(1, 2, 3)
max()
和min()
等内置函数可以接受一个迭代器参数,并返回最大或最小的元素。"in"
和"not in"
操作符也支持迭代器:X in iterator
为真,如果在迭代器返回的流中找到X。如果迭代器是无限的,你会遇到明显的问题; max()
、min()
永远不会返回,如果元素X从未出现在流中,则"in"
和"not in"
操作也不会返回。
注意,在迭代器中只能前进;没有办法获得上一个元素,重置迭代器,或者做一个副本。迭代器对象可以选择提供这些附加功能,但迭代器协议只指定__next__()
方法。函数因此可能会消耗所有迭代器的输出,如果你需要对同一个流做不同的事,你必须创建一个新的迭代器。
支持迭代器的数据类型 ¶
我们已经看到列表和元组如何支持迭代器。事实上,任何Python序列类型(例如字符串)都支持自动创建迭代器。
在字典上调用iter()
会返回一个遍历字典键的迭代器:
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Mar 3
Feb 2
Aug 8
Sep 9
Apr 4
Jun 6
Jul 7
Jan 1
May 5
Nov 11
Dec 12
Oct 10
请注意,顺序基本上是随机的,因为它基于字典中对象哈希值排序。
将iter()
应用于字典总是循环遍历键,但字典具有返回其他迭代器的方法。如果要迭代值或键/值对,可以显式调用values()
或items()
方法来获取合适的迭代器。
dict()
构造函数可以接受返回(key, value)
元组的有限流的迭代器:
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'US': 'Washington DC', 'France': 'Paris'}
文件也通过调用readline()
方法支持迭代,直到文件中没有更多的行。这意味着你可以如下所示读取文件的每一行:
for line in file:
# do something for each line
...
集合可以从迭代中获取其内容,并让你遍历集合的元素:
S = {2, 3, 5, 7, 11, 13}
for i in S:
print(i)
生成器表达式和列表推导式 ¶
迭代器输出上的两个常见操作是:1)对每个元素执行一些操作; 2)选择满足某些条件的元素子集。例如,给定一个字符串列表,你可能需要从每行中去除空格,或者提取包含给定子字符串的所有字符串。
列表推导式和生成器表达式(简写形式:“listcomps”和“genexps”)是用于这种操作的简明符号,从函数编程语言Haskell借鉴(https://www.haskell.org/)。你可以使用以下代码从字符串流中剥离所有空格:
line_list = [' line 1\n', 'line 2 \n', ...]
# Generator expression -- returns iterator
stripped_iter = (line.strip() for line in line_list)
# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]
你只能通过添加“if”
条件来选择某些元素:
stripped_list = [line.strip() for line in line_list
if line != ""]
用列表推导式,你得到一个Python列表; stripped_list
是一个包含生成的行的列表,而不是迭代器。生成器表达式返回一个迭代器,根据需要计算这些值,而不需要同时获得所有的值。这意味着如果你使用返回无限流或大量数据的迭代器,不要用列表推导式。在这些情况下优先选择生成器表达式。
生成器表达式用圆括号(“()”)包围,列表推导式用方括号(“[]”)包围。生成器表达式具有以下形式:
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3 ...
if condition3
for exprN in sequenceN
if conditionN )
再次说明,与列表推导式只有外部括号是不同的(方括号而不是圆括号)。
生成的元素将逐次输出expression
的值。if
子句都是可选的;如果存在,则只有condition
为真时,expression
才计算并添加到结果。
生成器表达式总是必须写在括号内,但是也算表示函数调用的括号。如果你想创建一个立即传递给函数的迭代器,你可以写成:
obj_total = sum(obj.count for obj in list_all_objects())
for...in
子句包含要迭代的序列。序列不必具有相同的长度,因为它们从左到右迭代,而不是并行地迭代。对于sequence1
中的每个元素,sequence2
从开始循环。然后,对来自sequence1
和sequence2
的每个结果对的元素循环迭代sequence3
。
换句话说,列表推导式或生成器表达式等同于以下Python代码:
for expr1 in sequence1:
if not (condition1):
continue # Skip this element
for expr2 in sequence2:
if not (condition2):
continue # Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue # Skip this element
# Output the value of
# the expression.
这意味着,如果有多个for...in
子句中,但没有if
子句,则结果输出的长度将等于所有序列长度的乘积。如果你有两个长度为3的列表,输出列表是9个元素的长度:
>>> seq1 = 'abc'
>>> seq2 = (1,2,3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]
为了避免在Python语法中引入歧义,如果expression
创建的是元组,它必须用圆括号括起来。下面的第一个列表推导式是一个语法错误,而第二个是正确的:
# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]
生成器¶
生成器是一个特殊类型的函数,可以简化编写迭代器的任务。常规函数计算一个值并返回它,但生成器返回一个迭代器,它返回一个值流。
你一定熟悉Python函数或C函数是如何被调用的。当你调用一个函数时,它会得到一个私有的命名空间,在那里它的局部变量被创建。当函数到达return
语句时,局部变量被销毁,并且该值返回给调用者。稍后调用相同的函数会创建一个新的私有命名空间和一组新的局部变量。但是,如果局部变量在退出函数时没有被抛弃呢?如果你从可以它离开的位置恢复这个函数,会是什么情况?这就是生成器提供的;它们可以被认为是可恢复的函数。
这里是生成器函数的最简单的例子:
>>> def generate_ints(N):
... for i in range(N):
... yield i
包含yield
关键字的任何函数都是生成器函数;这是由Python的字节码编译器检测到的,因为它会特殊编译该函数。
当调用生成器函数时,它不返回单个值;而是返回一个支持迭代器协议的生成器对象。在执行yield
表达式时,生成器输出i
的值,类似于return
语句。yield
和return
语句之间的巨大区别是,在到达yield
时,生成器的执行状态被暂停,并保留局部变量。在下一次调用生成器的__next__()
方法时,函数将恢复执行。
下面是generate_ints()
生成器的使用示例:
>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "stdin", line 1, in ?
File "stdin", line 2, in generate_ints
StopIteration
你可以等效地写成for i in generate_ints(5)
或者a,b,c = generate_ints(3)
。
在生成器函数内部,return value
会导致__next__()
方法引发StopIteration(value)
。一旦发生这种情况或者达到函数的底部,那么值的处理结束且生成器不能进一步产生任何值。
你可以通过编写自己的类并将生成器的所有局部变量存储为实例变量,手动实现生成器的效果。例如,返回整数列表可以通过将self.count
设置为0,并使__next__()
方法增加并返回self.count
。然而,对于一个适度复杂的生成器,编写一个相应的类可能更麻烦。
Python库中包含的测试套件Lib/test/test_generators.py包含了一些更有趣的示例。这里有一个生成器,使用生成器递归地实现树的中序遍历。
# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
在test_generators.py
中的两个其他示例产生N-Queens问题(将N个皇后放在NxN棋盘上,使得没有女王威胁另一个)和骑士之旅(找到一个骑士到NxN棋盘的每个方块的路线,而不会访问任何正方形两次)的解决方案。
将值传递到生成器¶
在Python 2.4和更早版本中,生成器只生成输出。一旦生成器的代码被调用以创建迭代器,当它的执行被恢复时,没有办法将任何新的信息传递给函数。你可以通过使生成器查看一个全局变量来获得这个能力,或通过传递进某个可变对象然后调用者修改它,但这些方法很混乱。
在Python 2.5中有一个简单的方法来传递值到生成器。yield
成为一个表达式,返回一个可以赋给变量或以其他方式操作的值:
val = (yield i)
我建议你在使用返回值执行某些操作时,始终在yield
表达式上放置括号,如上面的示例所示。括号并不总是必要的,但是始终加上它们很简单,而不必记住何时需要它们。
(PEP 342解释了确切的规则,即yield
表达式必须始终括号,除非它出现在赋值语句右边的顶级表达式。这意味着你可以写成val = yield i
,但是在有操作符时必须使用括号,如val = (yield i) + 12
。)
值通过调用其send(value)
方法发送到生成器。此方法恢复生成器的代码,yield
表达式返回指定的值。如果调用常规的__next__()
方法,则yield
返回None
。
这里是一个简单的计数器,以1递增,并允许更改内部计数器的值。
def counter(maximum):
i = 0
while i < maximum:
val = (yield i)
# If value provided, change counter
if val is not None:
i = val
else:
i += 1
下面是更改计数器的示例:
>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
File "t.py", line 15, in ?
it.next()
StopIteration
由于yield
通常会返回None
,因此应始终检查这种情况。除非你确定send()
方法是用于恢复生成器函数的唯一方法,否则在表达式中不要仅使用它的值。
除了send()
之外,生成器还有其他两个方法:
throw(type, value=None, traceback=None)
用于在生成器中引发异常;该异常在生成器的执行暂停时由yield
表达式引发。close()
在生成器中引发一个GeneratorExit
异常以终止迭代。收到此异常时,生成器的代码必须引发GeneratorExit
或StopIteration
;捕获异常并执行其他操作是非法的,并且将触发RuntimeError
。close()
也会在生成器被垃圾回收时被Python的垃圾收集器调用。如果在发生
GeneratorExit
时需要运行清理代码,我建议使用try: ... finally:
语句组,而不是捕获GeneratorExit
。
这些变化的累积效应是将生成器从单向信息生产者转变为生产者和消费者。
生成器也变成协程,一种更通用的子程序形式。子程序在一个点进入,并在另一个点退出(函数的顶部和return
语句),但协程可以在许多不同点进入、退出和恢复(yield
语句)。
内置函数 ¶
让我们更详细地了解一些常用于迭代器的内置函数。
Python的两个内置函数map()
和filter()
重复生成器表达式的特性:
map(f, iterA, iterB, ...)
返回序列上的一个迭代器f(iterA [0], iterB [0]), f(iterA [1], ]), f(iterA [2], iterB [2]), ...
。>>> def upper(s): ... return s.upper()
>>> list(map(upper, ['sentence', 'fragment'])) ['SENTENCE', 'FRAGMENT'] >>> [upper(s) for s in ['sentence', 'fragment']] ['SENTENCE', 'FRAGMENT']
你当然可以用列表推导式实现相同的效果。
filter(predicate, iter)
在返回一个满足一定条件的所有序列元素上的迭代器,可以由列表推导式类似地实现。predicate是返回某个条件的真值的函数;要与filter()
一起使用,predicate必须接收一个单一的值。
>>> def is_even(x):
... return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
这也可以写成列表推导式:
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]
enumerate(iter)
列举可迭代对象中的元素,返回包含序号和每个元素的2元组。
>>> for item in enumerate(['subject', 'verb', 'object']):
... print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')
enumerate()
通常用于遍历列表并记录满足特定条件的索引的时候:
f = open('data.txt', 'r')
for i, line in enumerate(f):
if line.strip() == '':
print('Blank line at line #%i' % i)
sorted(iterable, key=None, reverse=False)
将可迭代对象的所有元素收集到列表,对列表进行排序,并返回排序结果。key和reverse参数传递到构造的列表的sort()
方法。
>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]
(有关排序的详细讨论,请参阅排序HOWTO。)
any(iter)
和all(iter)
内置函数查看可迭代内容的真值。如果可迭代对象中的任何一个元素为真,则any()
返回True
;如果所有元素都为真,则all()
返回True
。
>>> any([0,1,0])
True
>>> any([0,0,0])
False
>>> any([1,1,1])
True
>>> all([0,1,0])
False
>>> all([0,0,0])
False
>>> all([1,1,1])
True
zip(iterA, iterB, ...)
从每个可迭代对象中取一个元素并一个元组返回它们:
zip(['a', 'b', 'c'], (1, 2, 3)) =>
('a', 1), ('b', 2), ('c', 3)
它不是在内存中构造一个列表并在返回之前耗尽所有输入迭代器;相反,元组只有在被请求时才被构造和返回。(这种行为的技术术语是延迟评估。)
此迭代器旨在与所有长度相同的可迭代对象一起使用。如果可迭代对象具有不同的长度,则所得到的流将具有与最短可迭代对象相同的长度。
zip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
你应该避免这样做,因为一个元素可能从较长的迭代器中取出并丢弃。这意味着你不能继续使用这个迭代器,因为你有可能跳过一个丢弃的元素。
itertools模块¶
itertools
模块包含许多常用的迭代器以及用于组合多个迭代器的函数。本节将通过展示几个小例子来介绍模块的内容。
该模块的函数分为几大类:
- 基于现有迭代器创建新迭代器的函数。
- 用于将迭代器的元素当作函数参数的函数。
- 选择迭代器部分输出的函数。
- 用于对迭代器输出进行分组的函数。
创建新的迭代器¶
itertools.count(n)
返回一个无限的整数流,每次增加1。你可以选择提供起始编号,默认为0:
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.cycle(iter)
保存所提供的可迭代对象的内容的副本,并返回一个新的迭代器,它从第一个到最后一个返回其元素。新的迭代器将无限地重复这些元素。
itertools.cycle([1,2,3,4,5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n])
返回提供的元素n次,如果未提供n则无限次返回。
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc
itertools.chain(iterA, iterB, ...)
以任意数量的可迭代对象作为输入,返回第一个迭代器的所有元素,然后返回第二个的所有元素,依此类推,直到所有迭代器都已用尽。
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice(iter, [start], stop, [step])
返回一个流,它是迭代器的一个切片。如果只有单个stop参数,它将返回第一个stop元素。如果提供起始索引,你将获得stop-start元素,如果你为step提供值,则元素将被相应跳过。与Python的字符串和列表切片不同,你不能对start、start和step使用负值。
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6
itertools.tee(iter, [n])
复制迭代器;它返回n个独立的迭代器,它们都将返回源迭代器的内容。如果不为n提供值,则默认值为2。复制迭代器需要保存源迭代器的一些内容,因此如果迭代器很大,并且其中一个新的迭代器消耗的比其他迭代器多,那么这会消耗大量的内存。
itertools.tee( itertools.count() ) =>
iterA, iterB
where iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
and iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
在元素上调用函数¶
operator
模块包含一组对应于Python运算符的函数。例如,operator.add(a, b)
(两个值相加)、operator.ne(a, b)
(等同于a != b
)和operator.attrgetter('id')
(返回一个获取.id
属性的可调用对象)。
itertools.starmap(func, iter)
假设可调用对象将返回一个元组流,并使用这些元组作为参数调用func:
itertools.starmap(os.path.join,
[('/bin', 'python'), ('/usr', 'bin', 'java'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby
选择元素¶
另一组函数基于谓词选择迭代器的元素的子集。
itertools.filterfalse(predicate, iter)
与filter()
相反,返回所有predicate返回假的元素:
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...
itertools.takewhile(predicate, iter)
不停地返回元素,只要predicate返回真。一旦predicate返回假,迭代器将结束。
def less_than_10(x):
return x < 10
itertools.takewhile(less_than_10, itertools.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
0
itertools.dropwhile(predicate, iter)
在predicate返回真时舍弃元素,然后返回其余迭代结果。
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
itertools.compress(data, selectors)
接收两个迭代器,只返回selectors对应的元素为真的data,每当任一迭代器用尽时停止:
itertools.compress([1,2,3,4,5], [True, True, False, False, True]) =>
1, 2, 5
组合函数¶
itertools.combinations(iterable, r)
返回一个迭代器,提供iterable中所有元素可能组合的r元组。
itertools.combinations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 3), (2, 4), (2, 5),
(3, 4), (3, 5),
(4, 5)
itertools.combinations([1, 2, 3, 4, 5], 3) =>
(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
(2, 3, 4), (2, 3, 5), (2, 4, 5),
(3, 4, 5)
每个元组中的元素保持与iterable返回的顺序相同。例如,在上述示例中,数字1总是在2、3、4和5之前。类似的函数,itertools.permutations(iterable, r=None)
,删除此约束,返回长度为r的所有可能的组合:
itertools.permutations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 1), (2, 3), (2, 4), (2, 5),
(3, 1), (3, 2), (3, 4), (3, 5),
(4, 1), (4, 2), (4, 3), (4, 5),
(5, 1), (5, 2), (5, 3), (5, 4)
itertools.permutations([1, 2, 3, 4, 5]) =>
(1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
...
(5, 4, 3, 2, 1)
如果不为r提供值,则使用可迭代对象的长度,这意味着所有元素都被重排。
注意,这些函数按位置产生所有可能的组合,并且不要求iterable的内容是唯一的:
itertools.permutations('aba', 3) =>
('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')
相同的元组('a', 'a', 'b')
出现两次,但是'a'字符串来自不同的位置。
itertools.combinations_with_replacement(iterable, r)
函数放宽了一个不同的约束:元素可以在单个元组中重复。概念上,为每个元组的第一位置选择元素,然后在选择第二元素之前替换元素。
itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
(2, 2), (2, 3), (2, 4), (2, 5),
(3, 3), (3, 4), (3, 5),
(4, 4), (4, 5),
(5, 5)
分组元素¶
我将讨论的最后一个函数是itertools.groupby(iter, key_func=None)
,它是最复杂的。key_func(elem)
是一个函数,可以计算由iter返回的每个元素的键值。如果你没有提供一个键函数,键就是每个元素本身。
groupby()
收集来自底层iter的所有具有相同键值的连续元素,并返回包含键值和具有该键的元素的迭代器的2元组流。
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]
def get_state(city_state):
return city_state[1]
itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...
where
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
groupby()
假定底层iter的内容已经基于键进行排序。注意,返回的迭代器也使用底层的iter,所以你必须在请求iterator-2及其相应的键之前使用完iterator-1的结果。
functools模块¶
Python 2.5中的functools
模块包含一些高阶函数。高阶函数将一个或多个函数作为输入,并返回一个新函数。此模块中最有用的工具是functools.partial()
函数。
对于以函数式样式编写的程序,有时需要构造已填充了某些参数的现有函数的变体。考虑一个Python函数f(a, b, c)
;你可能希望创建一个新的函数g(b, c)
等同于f(1, b, c)
;你在填充f()
某个参数的值。这被称为“部分函数应用”。
partial()
的构造函数接受参数(function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)
。生成的对象是可调用的,所以你可以调用它来以填充的参数调用function
。
这里有一个小但实际的例子:
import functools
def log(message, subsystem):
"""Write the contents of 'message' to the specified subsystem."""
print('%s: %s' % (subsystem, message))
...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')
functools.reduce(func, iter, [initial_value])
累积地对所有可迭代对象的元素执行操作, 因此不能应用于无限迭代。func必须是一个接受两个元素并返回单个值的函数。functools.reduce()
取迭代器返回的前两个元素A和B并计算func(A, B)
。然后它请求第三个元素C计算func(func(A, B), C)
,将此结果与返回的第四个元素组合,并继续直到迭代可用完为止。如果可迭代对象根本不返回任何值,则会引发TypeError
异常。如果提供了初始值,则将其用作起始点,并且func(initial_value, A)
是第一个计算。
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1,2,3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1
如果你在functools.reduce()
中使用operator.add()
,你会将迭代的所有元素相加。这种情况很常见,所以有一个特殊的内置函数sum()
来计算它:
>>> import functools, operator
>>> functools.reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0
对于functools.reduce()
的许多用途,可以更清楚地写出清晰的for
循环:
import functools
# Instead of:
product = functools.reduce(operator.mul, [1,2,3], 1)
# You can write:
product = 1
for i in [1,2,3]:
product *= i
相关的一个函数是itertools.accumulate(iterable, func=operator.add)。accumulate()
执行相同的计算,但不只是返回最终结果,而是返回一个迭代器产生每个部分的结果:
itertools.accumulate([1,2,3,4,5]) =>
1, 3, 6, 10, 15
itertools.accumulate([1,2,3,4,5], operator.mul) =>
1, 2, 6, 24, 120
小函数和lambda表达式¶
当编写函数式程序时,你经常需要一些函数作为谓词或者以某种方式组合元素。
如果有一个Python内置或模块函数是合适的,你不需要定义一个新的函数:
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
如果你需要的函数不存在,你需要编写它。写小函数的一种方法是使用lambda
语句。lambda
接收多个参数和一个组合这些参数的表达式,并创建一个返回表达式值的匿名函数:
adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)
另一种方法是只使用def
语句,并以通常的方式定义函数:
def adder(x, y):
return x + y
def print_assign(name, value):
return name + '=' + str(value)
哪个选择是更好的?这是一个风格问题;我通常的做法是避免使用lambda
。
我偏好的一个原因是lambda
在它可以定义的函数中非常有限。结果必须是可计算的单个表达式,这意味着你不能有多个if... elif... else
比较或try... except
语句。如果你试图在lambda
语句中做太多,你会得到一个过于复杂的表达式,很难阅读。快,下面的代码在做什么?
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]
你可以计算出来,但需要时间来解开表达式,以弄清楚发生了什么。使用短嵌套def
语句使事情更好一点:
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
但最好的是只使用for
循环:
total = 0
for a, b in items:
total += b
或sum()
内置函数和生成器表达式:
total = sum(b for a,b in items)
functools.reduce()
的许多用途对于写为for
循环时更清晰。
Fredrik Lundh曾经建议使用以下一组规则来重构lambda
的用法:
- 编写一个lambda函数。
- 写一个注释解释lambda做了什么。
- 花一段时间研究注释,并想一个能够捕捉到评论精髓的名字。
- 使用该名称将lambda转换为def语句。
- 删除评论。
我真的很喜欢这些规则,但你可以不同意这个没有lambda的风格是否更好。
修订历史和鸣谢¶
作者感谢以下人士为本文的各种草稿提供建议,更正和协助:Ian Bicking, Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell, Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton。
版本0.1:2006年6月30日发布。
版本0.11:2006年7月1日发布。修复笔误。
版本0.2:2006年7月10日发布。将genexp和listcomp节合并为一个。修复笔误。
版本0.21:添加了更多在教程邮件列表上建议的参考。
版本0.30:添加一节关于Collin Winter编写的functional
模块;添加一小节关于operator模块;一些其他编辑。
参考¶
一般的参考¶
Harold Abelson和Gerald Jay Sussman以及Julie Sussman的计算机程序的结构和解释。全文在https://mitpress.mit.edu/sicp/。在这本经典的计算机科学教科书中,第2章和第3章讨论了使用序列和流来组织程序内部的数据流。本书使用Scheme作为例子,但是这些章节中描述的许多设计方法适用于函数式Python代码。
http://www.defmacro.org/ramblings/fp.html:使用Java示例的函数式编程的一般介绍,具有冗长的历史介绍。
https://en.wikipedia.org/wiki/Functional_programming:描述函数式编程的一般Wikipedia条目。
https://en.wikipedia.org/wiki/Coroutine:协程条目。
https://en.wikipedia.org/wiki/Currying:Currying概念的条目。
特定于Python的¶
http://gnosis.cx/TPiP/:David Mertz的书Python中的文本处理的第一章讨论了文本处理的函数式编程,在标题为“利用文本处理中的高阶函数“这一节。
Mertz还为IBM的DeveloperWorks网站编写了一个3部分的关于函数式编程的文章;见第1部分,第2部分和第3部分