函数式编程 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,则 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 子句都是可选的;如果存在,expression 仅在 condition 为真时才会被求值并添加到结果中。

生成器表达式必须始终写在括号内,但表示函数调用的括号也算数。如果您想创建一个迭代器并立即将其传递给函数,您可以这样写

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 会导致从 __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 皇后问题(在 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;捕获异常并执行其他任何操作都是非法的,并将触发 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) 返回一个迭代器,遍历所有满足特定条件的序列元素,并且同样可以通过列表推导式复制。 **谓词** 是一个返回某个条件真值的函数;与 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() 如果可迭代对象中有任何元素为真值,则返回 Trueall() 如果所有元素都为真值,则返回 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 的字符串和列表切片不同,您不能对 *start*、*stop* 或 *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() 相反,返回谓词返回 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

组合函数

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

前面提到了 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 版本:增加了导师邮件列表中建议的更多参考文献。

0.30 版本:增加了由 Collin Winter 编写的 functional 模块一节;增加了 operator 模块的简短介绍;其他一些编辑。

参考文献

通用

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

https://defmacro.org/2006/06/19/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 中的新生成器特性。