函数式编程 HOWTO — Python 文档

来自菜鸟教程
Python/docs/3.8/howto/functional
跳转至:导航、​搜索

函数式编程HOWTO

作者
    1. 库克林

发布

0.32

在本文档中,我们将了解适合以函数式风格实现程序的 Python 特性。 在介绍了函数式编程的概念之后,我们将看看语言特性,例如 iterators 和 generators 以及相关的库模块,例如 itertoolsfunctools

介绍

本节解释函数式编程的基本概念; 如果您只是对了解 Python 语言功能感兴趣,请跳到下一部分 Iterators

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

  • 大多数编程语言是 过程化 :程序是指令列表,告诉计算机如何处理程序的输入。 C、Pascal 甚至 Unix shell 都是过程语言。
  • 声明性 语言中,您编写描述要解决的问题的规范,语言实现会弄清楚如何有效地执行计算。 SQL 是您最有可能熟悉的声明性语言; SQL 查询描述了您要检索的数据集,SQL 引擎决定是扫描表还是使用索引,应先执行哪些子条款等。
  • 面向对象的程序操作对象的集合。 对象具有内部状态并支持以某种方式查询或修改此内部状态的方法。 Smalltalk 和 Java 是面向对象的语言。 C++ 和 Python 是支持面向对象编程的语言,但不强制使用面向对象的特性。
  • 函数式 编程将问题分解为一组函数。 理想情况下,函数只接受输入并产生输出,并且没有任何影响给定输入产生的输出的内部状态。 著名的函数式语言包括 ML 系列(标准 ML、OCaml 和其他变体)和 Haskell。

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

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

有些语言对纯度非常严格,甚至没有a=3c = a + b等赋值语句,但很难避免所有副作用。 例如,打印到屏幕或写入磁盘文件是副作用。 例如,在 Python 中调用 print()time.sleep() 函数都没有返回有用的值; 他们只是因为向屏幕发送一些文本或暂停执行一秒钟的副作用而被调用。

以函数式风格编写的 Python 程序通常不会走到避免所有 I/O 或所有分配的极端; 相反,他们将提供一个功能性的界面,但会在内部使用非功能性的特性。 例如,函数的实现仍然会使用对局部变量的赋值,但不会修改全局变量或产生其他副作用。

函数式编程可以被认为是面向对象编程的对立面。 对象是包含一些内部状态以及允许您修改此状态的一组方法调用的小胶囊,程序由进行正确的状态更改集组成。 函数式编程希望尽可能避免状态更改,并处理函数之间的数据流动。 在 Python 中,您可以通过编写接受和返回表示应用程序中的对象(电子邮件消息、事务等)的实例的函数来组合这两种方法。

功能设计似乎是一个奇怪的工作限制。 为什么要避免对象和副作用? 函数式风格具有理论和实践优势:

  • 正式的可证明性。
  • 模块化。
  • 可组合性。
  • 易于调试和测试。

形式证明

理论上的好处是更容易构建函数式程序正确的数学证明。

长期以来,研究人员一直对寻找数学证明程序正确的方法感兴趣。 这不同于在大量输入上测试程序并得出其输出通常是正确的结论,或阅读程序的源代码并得出代码看起来正确的结论; 相反,目标是严格证明程序为所有可能的输入产生正确的结果。

用于证明程序正确的技术是写下 不变量 、输入数据和程序变量的属性,这些属性始终为真。 对于每一行代码,你然后证明如果不变量 X 和 Y 为真 before 行被执行,略有不同的不变量 X' 和 Y' 为真 after 行是执行。 这一直持续到程序结束,此时不变量应该与程序输出的所需条件相匹配。

函数式编程避免赋值的出现是因为这种技术很难处理赋值; 赋值可以破坏赋值之前为真的不变量,而不会产生任何可以向前传播的新不变量。

不幸的是,证明程序正确在很大程度上是不切实际的,并且与 Python 软件无关。 即使是微不足道的程序也需要几页长的证明; 中等复杂程序的正确性证明将是巨大的,并且您每天使用的程序(Python 解释器、您的 XML 解析器、您的 Web 浏览器)很少或没有一个可以被证明是正确的。 即使你写下或生成了一个证明,那么也会有验证这个证明的问题; 也许其中存在错误,并且您错误地认为您已经证明了该程序是正确的。


模块化

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


易于调试和测试

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

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

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


可组合性

在处理函数式程序时,您将编写许多具有不同输入和输出的函数。 其中一些功能不可避免地专门用于特定应用程序,但其他功能将在各种程序中有用。 例如,采用目录路径并返回目录中所有 XML 文件的函数,或者采用文件名并返回其内容的函数,可以应用于许多不同的情况。

随着时间的推移,您将形成一个个人实用程序库。 通常,您会通过在新配置中安排现有函数并编写一些专门用于当前任务的函数来组装新程序。


迭代器

我将首先研究 Python 语言特性,它是编写函数式程序的重要基础:迭代器。

迭代器是表示数据流的对象; 此对象一次返回一个元素的数据。 Python 迭代器必须支持名为 __next__() 的方法,该方法不接受任何参数并始终返回流的下一个元素。 如果流中没有更多元素,则 __next__() 必须引发 StopIteration 异常。 不过,迭代器不一定是有限的; 编写一个产生无限数据流的迭代器是完全合理的。

内置的 iter() 函数接受一个任意对象并尝试返回一个迭代器,该迭代器将返回该对象的内容或元素,如果该对象不支持迭代,则会引发 TypeError。 Python 的一些内置数据类型支持迭代,最常见的是列表和字典。 如果您可以获得一个迭代器,则该对象称为 iterable

您可以手动试验迭代界面:

>>> 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)

生成器表达式和列表推导式

迭代器输出的两个常见操作是 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 从头开始循环。 sequence3 然后对来自 sequence1sequence2 的每个结果元素对循环。

换句话说,列表推导式或生成器表达式等效于以下 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 的 bytecode 编译器检测到的,该编译器因此专门编译该函数。

当您调用生成器函数时,它不会返回单个值; 相反,它返回一个支持迭代器协议的生成器对象。 在执行 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 库中包含的测试套件 :source:`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 <module>
    it.next()
StopIteration

因为 yield 经常会返回 None,所以你应该经常检查这种情况。 不要只在表达式中使用它的值,除非您确定 send() 方法将是用于恢复生成器函数的唯一方法。

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

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

生成器也变成了 协程 ,一种更通用的子程序形式。 子程序在一个点进入并在另一个点退出(函数的顶部,和一个 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() 一起使用,谓词必须采用单个值。

>>> 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) 将iterable的所有元素收集到一个列表中,对列表进行排序,返回排序后的结果。 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]

(有关排序的更详细讨论,请参阅 Sorting HOW TO。)

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 相同)和 [ X117X]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) 在谓词返回真时丢弃元素,然后返回可迭代结果的其余部分。

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 的对应元素为真的那些元素,只要有一个就停止筋疲力尽:

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-2 及其对应的 key 之前使用 iterator-1 的结果。


功能工具模块

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

如果您将 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,布莱克温顿。

0.1 版:2006 年 6 月 30 日发布。

0.11 版:2006 年 7 月 1 日发布。 错别字修复。

0.2 版:2006 年 7 月 10 日发布。 将 genexp 和 listcomp 部分合并为一个。 错别字修复。

0.21 版:在导师邮件列表中添加了更多建议参考。

0.30版本:增加了Collin Winter编写的functional模块部分; 在操作员模块上添加了简短的部分; 其他一些编辑。


参考

一般

计算机程序的结构和解释 ,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:描述函数式编程的一般维基百科条目。

https://en.wikipedia.org/wiki/Coroutine:协程入口。

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


特定于 Python 的

http://gnosis.cx/TPiP/:David Mertz 的书 Text Processing in Python 的第一章讨论了文本处理的函数式编程,在标题为“利用高阶文本处理中的函数”。

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


Python 文档

itertools 模块的文档。

functools 模块的文档。

operator 模块的文档。

PEP 289:“生成器表达式”

PEP 342:“Coroutines via Enhanced Generators”描述了 Python 2.5 中的新生成器特性。