函数式编程 HOWTO

作者:

A. M. Kuchling

版本:

0.32

在本文件中,我们将介绍 Python 中适合实现函数式风格程序的功能。在介绍函数式编程的概念之后,我们将介绍语言特性,例如 迭代器生成器,以及相关的库模块,例如 itertoolsfunctools.

简介

本节解释了函数式编程的基本概念;如果您只对学习 Python 语言特性感兴趣,请跳过下一节关于 迭代器 的内容。

编程语言支持以多种不同的方式分解问题

  • 大多数编程语言是过程式的:程序是指令列表,告诉计算机如何处理程序的输入。C、Pascal 甚至 Unix shell 都是过程式语言。

  • 声明式语言中,您编写一个规范来描述要解决的问题,而语言实现会找出如何有效地执行计算。SQL 是您最有可能熟悉的声明式语言;SQL 查询描述了您想要检索的数据集,而 SQL 引擎决定是扫描表还是使用索引,哪些子句应该首先执行等等。

  • 面向对象程序操作对象集合。对象具有内部状态,并支持查询或以某种方式修改此内部状态的方法。Smalltalk 和 Java 是面向对象的语言。C++ 和 Python 是支持面向对象编程的语言,但不会强制使用面向对象的功能。

  • 函数式编程将问题分解成一组函数。理想情况下,函数只接受输入并产生输出,并且没有任何内部状态会影响为给定输入产生的输出。众所周知的函数式语言包括 ML 家族(Standard ML、OCaml 和其他变体)和 Haskell。

一些计算机语言的设计者选择强调一种特定的编程方法。这通常使得编写使用不同方法的程序变得困难。其他语言是多范式语言,支持多种不同的方法。Lisp、C++ 和 Python 是多范式的;您可以在所有这些语言中编写主要过程式、面向对象或函数式的程序或库。在一个大型程序中,不同的部分可能使用不同的方法编写;例如,GUI 可能面向对象,而处理逻辑可能是过程式或函数式的。

在函数式程序中,输入流经一组函数。每个函数对其输入进行操作并产生一些输出。函数式风格不鼓励具有副作用的函数,这些副作用会修改内部状态或进行其他不在函数返回值中可见的更改。完全没有副作用的函数称为纯函数。避免副作用意味着不使用在程序运行时更新的数据结构;每个函数的输出必须只依赖于其输入。

一些语言对纯度要求非常严格,甚至没有赋值语句,例如 a=3c = a + b,但很难避免所有副作用,例如打印到屏幕或写入磁盘文件。另一个例子是调用 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 <module>
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])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

请注意,从 Python 3.7 开始,字典迭代顺序保证与插入顺序相同。在早期版本中,行为未指定,并且可能在不同的实现之间有所不同。

iter() 应用于字典始终循环遍历键,但字典具有返回其他迭代器的方法。如果你想遍历值或键/值对,你可以显式调用 values()items() 方法以获取适当的迭代器。

dict() 构造函数可以接受一个迭代器,该迭代器返回一个有限的 (key, value) 元组流。

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

文件也支持通过调用 readline() 方法进行迭代,直到文件中没有更多行。这意味着你可以像这样读取文件的每一行。

for line in file:
    # do something for each line
    ...

集合可以从可迭代对象中获取其内容,并允许你遍历集合的元素。

>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
...     print(i)
2
3
5
7
11
13

生成器表达式和列表推导

对迭代器输出的两个常见操作是 1) 对每个元素执行某些操作,2) 选择满足某些条件的元素子集。例如,给定一个字符串列表,你可能希望从每行中剥离尾随空格,或提取包含给定子字符串的所有字符串。

列表推导和生成器表达式(简称:“listcomps” 和“genexps”)是此类操作的简洁表示法,借鉴自函数式编程语言 Haskell (https://www.haskell.org/)。你可以使用以下代码从字符串流中剥离所有空格。

>>> line_list = ['  line 1\n', 'line 2  \n', ' \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 从头开始循环。然后,对于 sequence1sequence2 中的每个结果元素对, 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 语句。 yieldreturn 语句之间的最大区别在于,在到达 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 <module>
  File "stdin", line 2, in generate_ints
StopIteration

你也可以写 for i in generate_ints(5),或者 a, b, c = generate_ints(3)

在生成器函数内部, return value 会导致 StopIteration(value)__next__() 方法中引发。一旦发生这种情况,或者到达函数的底部,值的进程就会结束,生成器不能再产生任何值。

你可以通过编写自己的类并将生成器中的所有局部变量存储为实例变量来手动实现生成器的效果。例如,返回一个整数列表可以通过将 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 皇后问题(在 NxN 的棋盘上放置 N 个皇后,使任何两个皇后都不互相攻击)和骑士巡游问题(找到一条路线,让骑士访问 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 <module>
    it.next()
StopIteration

由于 yield 通常会返回 None,因此你应该始终检查这种情况。除非你确定 send() 方法将是恢复生成器函数的唯一方法,否则不要在表达式中直接使用它的值。

除了 send() 之外,生成器还有另外两种方法。

  • throw(value) 用于在生成器内部引发异常;异常是在生成器暂停执行的 yield 表达式处引发的。

  • close() 在生成器内部引发 GeneratorExit 异常以终止迭代。在接收到此异常后,生成器的代码必须引发 GeneratorExitStopIteration;捕获异常并执行其他操作是非法的,将触发 RuntimeError。当生成器被垃圾回收时,Python 的垃圾收集器也会调用 close()

    如果您需要在发生 GeneratorExit 时运行清理代码,我建议使用 try: ... finally: 语句,而不是捕获 GeneratorExit

这些更改的累积效果是将生成器从单向信息生产者转变为生产者和消费者。

生成器也变成了协程,这是子例程的更通用形式。子例程从一个点进入,从另一个点退出(函数的顶部和 return 语句),但协程可以在许多不同的点进入、退出和恢复(yield 语句)。

内置函数

让我们更详细地了解一下通常与迭代器一起使用的内置函数。

Python 的两个内置函数,map()filter() 复制了生成器表达式的功能。

map(f, iterA, iterB, ...) 返回一个迭代器,该迭代器遍历序列

f(iterA[0], iterB[0]), f(iterA[1], iterB[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) 返回一个迭代器,该迭代器遍历所有满足特定条件的序列元素,并且类似地被列表推导复制。谓词是一个返回某个条件真值的函数;用于 filter() 时,谓词必须接受单个值。

>>> 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, start=0) 统计可迭代对象中的元素,返回包含计数(从 start 开始)和每个元素的 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) 将可迭代对象的所有元素收集到一个列表中,对列表进行排序,并返回排序后的结果。keyreverse 参数传递给构造的列表的 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]

(有关排序的更详细讨论,请参见 排序技术。)

内置函数 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(start, step) 返回一个无限的等间距值流。您可以选择提供起始数字,默认为 0,以及数字之间的间隔,默认为 1

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.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

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 的字符串和列表切片不同,您不能对 startstopstep 使用负值。

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, ...

在元素上调用函数

The 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() 相反,返回谓词返回 false 的所有元素

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) 返回元素,只要谓词返回 true。一旦谓词返回 false,迭代器将发出其结果结束的信号。

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) 在谓词返回 true 时丢弃元素,然后返回可迭代对象结果的其余部分。

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) 接受两个迭代器,仅返回 data 中对应 selectors 元素为 true 的元素,并在任一迭代器耗尽时停止

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

组合函数

The 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) 是一个函数,可以为可迭代对象返回的每个元素计算一个键值。如果您没有提供键函数,则键只是每个元素本身。

groupby() 收集来自底层可迭代对象的具有相同键值的所有连续元素,并返回一个包含键值和具有该键的元素迭代器的 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() 假设底层可迭代对象的内容将根据键进行排序。请注意,返回的迭代器也使用底层可迭代对象,因此您必须在请求迭代器 2 及其对应键之前使用迭代器 1 的结果。

functools 模块

模块 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

如果您使用 operator.add()functools.reduce() 一起使用,您将添加可迭代对象的所有元素。这种情况非常普遍,因此有一个特殊的内置函数称为 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

运算符模块

operator 模块之前已经提到过。它包含一组与 Python 运算符相对应的函数。这些函数在函数式代码中经常很有用,因为它们可以让你免于编写执行单个操作的琐碎函数。

此模块中的一些函数是

  • 数学运算:add()sub()mul()floordiv()abs()、…

  • 逻辑运算:not_()truth()

  • 按位运算:and_()or_()invert()

  • 比较:eq()ne()lt()le()gt()ge()

  • 对象标识:is_()is_not()

有关完整列表,请参阅运算符模块的文档。

小型函数和 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 使用的规则:

  1. 编写一个 lambda 函数。

  2. 编写一个注释来解释这个 lambda 函数到底做了什么。

  3. 仔细研究一下注释,想一个能概括注释本质的名字。

  4. 将 lambda 函数转换为一个 def 语句,使用那个名字。

  5. 删除注释。

我非常喜欢这些规则,但你可以自由地不同意这种无 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:根据 tutor 邮件列表的建议添加了更多参考资料。

版本 0.30:添加了 Collin Winter 编写的 functional 模块部分;添加了关于 operator 模块的简短部分;其他一些编辑。

参考资料

通用

计算机程序的结构和解释,作者 Harold Abelson 和 Gerald Jay Sussman 与 Julie Sussman。这本书可以在 https://mitpress.mit.edu/sicp 找到。在这本经典的计算机科学教科书中,第 2 章和第 3 章讨论了使用序列和流来组织程序内部的数据流。这本书使用 Scheme 来举例,但这些章节中描述的许多设计方法都适用于函数式 Python 代码。

https://www.defmacro.org/ramblings/fp.html:一篇使用 Java 示例的函数式编程入门文章,包含了冗长的历史介绍。

https://en.wikipedia.org/wiki/Functional_programming:描述函数式编程的维基百科通用条目。

https://en.wikipedia.org/wiki/Coroutine:协程的条目。

https://en.wikipedia.org/wiki/Partial_application:部分函数应用概念的条目。

https://en.wikipedia.org/wiki/Currying:柯里化概念的条目。

Python 特定

https://gnosis.cx/TPiP/:David Mertz 的书《Python 文本处理》的第一章讨论了用于文本处理的函数式编程,在名为“在文本处理中利用高阶函数”的部分。

Mertz 还为 IBM 的 DeveloperWorks 网站撰写了一系列关于函数式编程的 3 部分文章;见 第 1 部分第 2 部分第 3 部分

Python 文档

itertools 模块的文档。

functools 模块的文档。

operator 模块的文档。

PEP 289: “生成器表达式”

PEP 342: “通过增强生成器实现协程” 描述了 Python 2.5 中新的生成器特性。