函数式编程

作者

库克林

释放

0.32

在本文档中,我们将介绍适合于以功能风格实现程序的Python特性。在介绍了函数式编程的概念之后,我们将了解诸如 iterator S和 generator 和相关的库模块,如 itertoolsfunctools .

介绍

本节介绍了函数式编程的基本概念;如果您只是想了解Python语言特性,请跳到下一节 遍历器 .

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

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

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

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

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

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

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

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

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

函数式编程可以被视为与面向对象编程相反。对象是包含一些内部状态的小Capsules,以及允许您修改此状态的方法调用集合,而程序由进行正确的状态更改集组成。函数式编程希望尽可能避免状态变化,并使用函数之间的数据流。在Python中,您可以通过编写函数来组合这两种方法,这些函数获取并返回表示应用程序中对象的实例(电子邮件、事务等)。

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

  • 正式证明。

  • 模块性。

  • 可组合性。

  • 易于调试和测试。

形式证明性

理论上的好处是,构造一个证明函数程序正确的数学证明更容易。

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

用来证明程序正确的技术是写下 不变性 ,输入数据和程序变量的属性始终为真。对于每一行代码,然后显示如果不变量x和y为真 before 行被执行,稍微不同的不变量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 <module>
StopIteration
>>>

python期望在几个不同的上下文中有可ITerable对象,最重要的是 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,则为true。如果迭代器是无限的,您会遇到明显的问题; 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
    ...

集合可以从iterable获取其内容,并让您迭代集合的元素:

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 条款都是可选的;如果存在, expression 只有当 condition 是真的。

生成器表达式总是必须写在括号内,但表示函数调用的括号也会计数。如果要创建将立即传递给函数的迭代器,可以编写:

obj_total = sum(obj.count for obj in list_all_objects())

这个 for...in 子句包含要迭代的序列。序列的长度不必相同,因为它们是从左到右循环的, not 并行地。对于中的每个元素 sequence1sequence2 从一开始就是循环的。 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 语句。两者之间的巨大区别 yield 和A return 声明是在达到 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棋盘的每一个方格,而无需访问任何一个方格两次)。

将值传递到生成器中

在Python2.4及更早版本中,生成器只生成输出。一旦调用了生成器的代码来创建迭代器,就无法在函数恢复执行时将任何新信息传递给函数。您可以通过让生成器看一个全局变量,或者通过传入调用者随后修改的某个可变对象来破解这种能力,但是这些方法是混乱的。

在Python2.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() ,生成器上还有两种其他方法:

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

生成器也变成 coroutines 子程序的一种更通用的形式。子例程在一个点输入,在另一个点(函数的顶部,以及 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) 对iterable中的元素进行计数,返回包含计数的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 如果iterable中的任何元素是真值,并且 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, ...) 从每个iterable中提取一个元素,并以元组的形式返回它们:

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

它不会构造内存中的列表,并在返回之前耗尽所有输入迭代器;相反,只有在请求元组时才会构造和返回它们。(这种行为的技术术语是 lazy evaluation ②)

此迭代器用于长度相同的iterables。如果iterables的长度不同,则生成的流将与最短iterables的长度相同。::

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

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, ...) 以任意数量的iterables作为输入,并返回第一个迭代器的所有元素,然后返回第二个迭代器的所有元素,依此类推,直到所有iterables都用尽。::

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的字符串和列表切片不同,不能对 开始stopstep . ::

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 != boperator.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() ,返回谓词返回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时丢弃元素,然后返回ITerable结果的其余部分。::

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) 返回一个迭代器,给出所有可能的 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)

每个元组中的元素的顺序与 可迭代的 还给他们。例如,在上面的示例中,数字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) 是一个函数,可以为iterable返回的每个元素计算键值。如果不提供键函数,则键只是每个元素本身。

groupby() 从具有相同键值的基础iterable中收集所有连续元素,并返回包含键值的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的内容已经基于键进行排序。注意,返回的迭代器也使用基础的iterable,因此在请求迭代器-2及其对应的键之前,必须使用迭代器-1的结果。

功能工具模块

这个 functools python 2.5中的模块包含一些高阶函数。一 higher-order function 接受一个或多个函数作为输入并返回一个新函数。本模块中最有用的工具是 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]) 累积地对所有iterable的元素执行操作,因此不能应用于无限iterable。 func 必须是接受两个元素并返回单个值的函数。 functools.reduce() 获取迭代器返回的前两个元素a和b并计算 func(A, B) .然后它请求第三个元素c,计算 func(func(A, B), C) ,将此结果与返回的第四个元素组合,并继续,直到ITerable耗尽。如果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() ,您将添加iterable的所有元素。这种情况很常见,有一个特殊的内置名 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样式是否更好,您可以自由地提出异议。

修订历史和确认

作者要感谢以下人员对本文的各种草稿提供建议、更正和帮助:伊恩•比金、尼克•科格伦、尼克•埃佛德、雷蒙德•赫廷格、吉姆•杰维特、迈克•克莱尔、莱安德罗•拉米罗、朱西•萨尔梅拉、科林•温特、布莱克•温顿。

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

0.11版:2006年7月1日发布。键入修复。

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

0.21版:在导师邮件列表中添加了更多推荐信。

0.30版:在 functional 由科林·温特编写的模块;在操作员模块上添加了简短的部分;其他一些编辑。

工具书类

一般

计算机程序的结构和解释 哈罗德·亚伯森和杰拉尔德·杰伊·苏斯曼与朱莉·苏斯曼合著。全文位于https://mitpress.mit.edu/sicp/。在这本经典的计算机科学教科书中,第2章和第3章讨论了使用序列和流来组织程序内的数据流。本书使用Scheme作为例子,但是这些章节中描述的许多设计方法都适用于函数式的Python代码。

http://www.defmacro.org/ramblings/fp.html: A general introduction to functional programming that uses Java examples and has a lengthy historical introduction.

https://en.wikipedia.org/wiki/Functional_programming: General Wikipedia entry describing functional programming.

https://en.wikipedia.org/wiki/Coroutine: Entry for coroutines.

https://en.wikipedia.org/wiki/Currying: Entry for the concept of currying.

特定的 Python

http://gnosis.cx/TPiP/: The first chapter of David Mertz's book Text Processing in Python 在题为“在文本处理中使用高阶函数”的章节中,讨论了文本处理的函数编程。

Mertz还为IBM的developerWorks站点编写了一系列由3部分组成的关于函数式编程的文章;请参见 part 1part 2part 3

python文档

文件 itertools 模块。

文件 functools 模块。

文件 operator 模块。

PEP 289 :“生成器表达式”

PEP 342 :“通过增强的生成器进行协同工作”描述了Python2.5中新的生成器特性。