函数式编程 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 浏览器)很少或没有可以证明是正确的。即使您写下或生成了证明,也会有验证证明的问题;也许其中存在错误,并且您错误地认为您已经证明了该程序是正确的。

模块化

函数式编程更实际的好处是它迫使您将问题分解为小块。程序因此更模块化。指定和编写一个做一件事的小函数比编写一个执行复杂转换的大函数更容易。小函数也更容易阅读和检查错误。

易于调试和测试

测试和调试函数式风格的程序更容易。

调试得以简化,因为函数通常很小且明确指定。当程序无法工作时,每个函数都是一个接口点,您可以在其中检查数据是否正确。您可以查看中间输入和输出,以快速隔离导致 bug 的函数。

测试更容易,因为每个函数都是单元测试的潜在主题。函数不依赖于需要在运行测试之前复制的系统状态;相反,您只需要合成正确的输入,然后检查输出是否符合预期。

可组合性

当您处理函数式风格的程序时,您将编写许多具有不同输入和输出的函数。其中一些函数将不可避免地专门用于特定的应用程序,但其他函数将在各种程序中都很有用。例如,一个接受目录路径并返回该目录中所有 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 在迭代器返回的流中找到,则 X in iterator 为真。如果迭代器是无限的,您会遇到明显的问题;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) 选择满足某些条件的元素子集。例如,给定一个字符串列表,您可能希望从每行中去除尾随的空白字符,或提取包含给定子字符串的所有字符串。

列表推导式和生成器表达式(简称:“列表推导”和“生成表达式”)是此类操作的简洁表示法,借鉴自函数式编程语言 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 皇后问题(将 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 <module>
    it.next()
StopIteration

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

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

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

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

    如果您需要在发生 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) 返回一个迭代器,该迭代器包含所有满足特定条件的序列元素,并且类似地可以通过列表推导式复制。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, 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, ...

在元素上调用函数

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

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

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() 假设底层可迭代对象的内容将已根据键排序。请注意,返回的迭代器也使用底层可迭代对象,因此你必须先使用 iterator-1 的结果,然后再请求 iterator-2 及其对应的键。

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

前面提到了 operator 模块。它包含一组与 Python 的运算符对应的函数。这些函数在函数式风格的代码中通常很有用,因为它们可以避免你编写执行单个操作的简单函数。

此模块中的一些函数是

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

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

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

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

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

请参阅 operator 模块的文档以获取完整列表。

小函数和 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 中的新生成器功能。