Skip to main content

功能编程HOWTO

Author:

A. M. Kuchling

Release:

0.32

在本文中,我们将介绍Python适用于以函数式样实现程序的功能。在介绍函数编程的概念之后,我们将介绍 iteratorgenerator 等语言特性以及 itertoolsfunctools 等相关库模块。

介绍

本节介绍功能编程的基本概念;如果你只是想学习Python语言特性,请跳到 迭代器 的下一节。

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

  • 大多数编程语言是 程序:程序是指令列表,告诉计算机如何处理程序的输入。 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为真 之前,则执行该行,稍微不同的不变量X’和Y’为真 ,则执行该行。这将持续到您到达程序的结尾,此时不变量应该匹配程序输出上的期望条件。

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

不幸的是,验证程序正确是很大程度上不切实际,与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 ?
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])
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 从开始循环。然后针对来自 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的 bytecode 编译器检测到的,它特别编译了函数作为结果。

当调用generator函数时,它不返回单个值;而是返回一个支持迭代器协议的生成器对象。在执行 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 ?
  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-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(),还有两种其他的发电机方法:

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

发电机也成为 协同,一种更通用的子程序形式。子程序在一个点输入,在另一个点退出(函数的顶部和 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) 对迭代中的元素进行计数,返回包含count和每个元素的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]

(有关排序的更详细讨论,请参阅 排序如何。)

any(iter)all(iter) 内置函数查看iterable的内容的真值。如果迭代中的任何元素是真值,则 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)

它不构造一个内存列表,并在返回之前耗尽所有输入迭代器;相反,元组只有在被请求时才被构造和返回。 (这个行为的技术术语是 懒惰评价。)

此迭代器旨在与所有长度相同的iterables一起使用。如果迭代具有不同的长度,则所得到的流将具有与最短可迭代的相同的长度。

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) 保存所提供的iterable的内容的副本,并返回一个新的迭代器,它返回其元素从first到last。新的迭代器将无限地重复这些元素。

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) 假设iterable将返回一个元组流,并使用这些元组作为参数调用 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() 相反,返回谓词返回为假的所有元素:

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

只要谓词返回true,itertools.takewhile(predicate, iter) 就返回元素。一旦谓词返回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 为真的那些元素,每当任何一个元素耗尽时:

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() 假定底层可迭代的内容已经基于键进行排序。注意返回的迭代器也使用底层的iterable,所以你必须在请求iterator-2及其相应的键之前使用iterator-1的结果。

functools模块

Python 2.5中的 functools 模块包含一些高阶函数。 高阶函数 使用一个或多个函数作为输入并返回一个新函数。此模块中最有用的工具是 functools.partial() 函数。

对于以函数式样编写的程序,有时需要构造已经填充了一些参数的现有函数的变体。考虑一个Python函数 f(a, b, c);您可能希望创建一个等效于 f(1, b, c) 的新函数 g(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),将该结果与返回的第四元素组合,并且继续,直到可迭代被耗尽。如果iterable根本不返回任何值,则会引发 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) <itertools.accumulate。它执行相同的计算,但是不是仅返回最终结果,而是 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)

当写为 for 循环时,functools.reduce() 的许多使用更清楚。

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 模块上添加一个部分;在操作员模块上添加短节;一些其他编辑。

参考文献

一般

计算机程序的结构和解释,哈罗德·阿伯尔森和杰拉德·杰斯·苏斯曼与朱莉·苏斯曼。全文在 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:currying的概念的入口。

特定于Python

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

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

Python文档

itertools 模块的文档。

operator 模块的文档。

PEP 289:“生成器表达式”

PEP 342:“Coroutines via Enhanced Generators”介绍了Python 2.5中的新生成器功能。