函数式编程指南

  • Author

    • 库克林
  • Release

    • 0.31

在本文档中,我们将浏览适用于以Function样式实现程序的 Python Function。在介绍了函数式编程的概念之后,我们将介绍诸如iterator s 和generator s 之类的语言Function以及诸如itertoolsfunctools之类的相关库模块。

Introduction

本节说明Function编程的基本概念;如果您只是想了解 Python 语言Function,请跳到下一部分。

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

  • 大多数编程语言都是“程序性的”:程序是指令列表,它们告诉计算机如何处理程序的 Importing。 C,Pascal 甚至 Unix Shell 是过程语言。

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

  • 面向对象 程序可操纵对象的集合。对象具有内部状态和支持方法,这些方法以某种方式查询或修改此内部状态。 Smalltalk 和 Java 是面向对象的语言。 C 和 Python 是支持面向对象编程的语言,但不强制使用面向对象的Function。

  • Function 编程将问题分解为一组Function。理想情况下,函数仅接受 Importing 并产生输出,而没有任何内部状态会影响给定 Importing 所产生的输出。众所周知的Function语言包括 ML 系列(标准 ML,OCaml 和其他变体)和 Haskell。

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

在Function程序中,Importing 流经一组Function。每个函数在其 Importing 上进行操作并产生一些输出。函数样式不建议使用带有副作用的函数,这些副作用会修改内部状态或进行其他在函数的返回值中不可见的更改。完全没有副作用的Function称为“纯Function”。避免副作用意味着不使用在程序运行时会更新的数据结构。每个函数的输出必须仅取决于其 Importing。

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

以函数式风格编写的 Python 程序通常不会避免所有 I/O 或所有分配。取而代之的是,它们将提供Function正常的界面,但会在内部使用非Function性Function。例如,函数的实现仍将使用对局部变量的赋值,但不会修改全局变量或具有其他副作用。

函数式编程可以被认为与面向对象编程相反。对象是包含一些内部状态以及使您可以修改此状态的方法调用集合的小型封装,程序包括对状态进行正确的设置。函数式编程希望尽可能避免状态变化,并使用函数之间的数据流。在 Python 中,您可以pass编写函数来结合这两种方法,这些函数接受和返回代表应用程序中的对象的实例(电子邮件,Transaction 等)。

Function设计似乎是一个奇怪的约束条件。为什么要避免物体和副作用?Function样式具有理论和实践上的优势:

  • Formal provability.

  • Modularity.

  • Composability.

  • 易于调试和测试。

Formal provability

从理论上讲,好处是可以更容易地构建Function程序正确的 math 证明。

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

用来证明程序正确的技术是写下“不变式”,Importing 数据的属性以及程序变量始终为真的属性。然后,对于每一行代码,如果在执行该行之前 ,如果不变量 X 和 Y 为 true,则在执行该行之后 ,稍有不同的不变量 X'和 Y'为 true。这一直持续到您到达程序末尾为止,此时不变量应与程序输出中的所需条件匹配。

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

不幸的是,证明程序正确是不切实际的,并且与 Python 软件无关。即使是琐碎的程序也需要长达数页的证明。一个中等复杂的程序的正确性的证明将是巨大的,并且您日常使用的程序(Python 解释器,XML 解析器,Web 浏览器)很少或没有被证明是正确的。即使您写下或生成了证明,也将存在验证证明的问题。也许其中有错误,并且您错误地认为自己已证明该程序正确。

Modularity

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

易于调试和测试

测试和调试Function样式的程序更加容易。

由于Function通常较小且明确指定,因此简化了调试。当程序不起作用时,每个Function都是一个接口点,您可以在其中检查数据是否正确。您可以查看中间 Importing 和输出,以快速隔离导致错误的Function。

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

Composability

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

随着时间的流逝,您将形成一个 Util 的个人库。通常,您将pass以新配置安排现有Function并编写一些专门用于当前任务的Function来组装新程序。

Iterators

我将从查看 Python 语言Function开始,它是编写函数式程序的重要基础:迭代器。

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

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

您可以手动try迭代界面:

>>> L = [1,2,3]
>>> it = iter(L)
>>> print it
<...iterator object at ...>
>>> it.next()
1
>>> it.next()
2
>>> it.next()
3
>>> it.next()
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()构造函数将迭代器具体化为列表或 Tuples:

>>> L = [1,2,3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

序列解压缩还支持迭代器:如果您知道迭代器将返回 N 个元素,则可以将它们解压缩为 N 个 Tuples:

>>> 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为 true。如果迭代器是无限的,则会遇到明显的问题; max()min()将永远不会返回,并且如果元素 X 永远不会出现在流中,则"in""not in"运算符也不会返回。

注意,您只能在迭代器中前进;无法获取上一个元素,重置迭代器或对其进行复制。迭代器对象可以选择提供这些附加Function,但是迭代器协议仅指定next()方法。因此,函数可能会消耗迭代器的所有输出,并且如果您需要对同一流执行其他操作,则必须创建一个新的迭代器。

支持迭代器的数据类型

我们已经了解了列表和 Tuples 如何支持迭代器。实际上,任何 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

请注意,Sequences 实际上是随机的,因为它是基于字典中对象的哈希 Sequences 的。

iter()应用于字典总是会在键上循环,但是字典中的方法会返回其他迭代器。如果要遍历键,值或键/值对,则可以显式调用iterkeys()itervalues()iteritems()方法以获取适当的迭代器。

dict()构造函数可以接受返回(key, value)Tuples 的有限流的迭代器:

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'US': 'Washington DC', 'France': 'Paris'}

文件还pass调用readline()方法来支持迭代,直到文件中没有更多行为止。这意味着您可以像这样读取文件的每一行:

for line in file:
    # do something for each line
    ...

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

S = set((2, 3, 5, 7, 11, 13))
for i in S:
    print i

生成器表达式和列表推导

迭代器输出的两个常见操作是:1)对每个元素执行某种操作,2)选择满足某种条件的元素子集。例如,给定一个字符串列表,您可能希望从每一行中删除尾随空格或提取包含给定子字符串的所有字符串。

列表推导和生成器表达式(简称:“ listcomps”和“ genexps”)是从Function编程语言 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]

您可以pass添加"if"条件来仅选择某些元素:

stripped_list = [line.strip() for line in line_list
                 if line != ""]

pass列表理解,您将获得一个 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为 true 时评估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正在创建 Tuples,则必须用括号将其括起来。下面的第一个列表理解是语法错误,而第二个是正确的:

# Syntax error
[ x,y for x in seq1 for y in seq2]
# Correct
[ (x,y) for x in seq1 for y in seq2]

Generators

生成器是一类特殊的函数,可简化编写迭代器的任务。常规函数计算一个值并返回它,但是生成器返回一个迭代器,该迭代器返回一个值流。

毫无疑问,您熟悉常规函数在 Python 或 C 中的工作方式。调用函数时,它将获得一个私有命名空间,用于创建其局部变量。当函数到达return语句时,局部变量将被销毁,并将值返回给调用方。以后对同一函数的调用将创建一个新的专用命名空间和一组新的局部变量。但是,如果在退出函数时没有扔掉局部变量怎么办?如果您以后可以在break的地方 continue 运行该怎么办?这就是 Generator 提供的;可以将它们视为可恢复的Function。

这是生成器函数的最简单示例:

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 ...>
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
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语句只能在不带值的情况下使用,并指示值的处理结束。执行return后,生成器无法返回任何其他值。 return的值(例如return 5)是生成器函数内部的语法错误。生成器结果的结尾也可以pass手动提高StopIteration或仅让执行流程脱离函数的底部来指示。

pass编写自己的类并将生成器的所有局部变量存储为实例变量,可以手动实现生成器的效果。例如,返回整数列表可以pass将self.count设置为 0,并使next()方法递增self.count并返回来完成。但是,对于中等复杂的生成器,编写相应的类可能会更加麻烦。

Python 库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 棋盘的每个正方形的 Route)的解决方案任何正方形两次)。

将值传递到生成器

在 Python 2.4 和更早版本中,生成器仅生成输出。一旦调用了生成器的代码以创建迭代器,就无法在恢复执行时将任何新信息传递给函数。您可以pass使生成器查看全局变量或传入调用者随后进行修改的可变对象,来共同利用此Function,但是这些方法很混乱。

在 Python 2.5 中,有一种简单的方法可以将值传递给生成器。 yield成为表达式,返回可以分配给变量或对其进行以下操作的值:

val = (yield i)

我建议您在使用返回的值进行操作时,总是将括号放在yield表达式上,如上例所示。括号并不总是必需的,但是总是添加括号而不是记住何时需要它们会更容易。

(PEP 342 解释了确切的规则,即yield表达式必须始终用括号括起来,除非它出现在赋值右侧的顶级表达式上.这意味着您可以编写val = yield i,但必须使用括号进行操作时,如val = (yield i) + 12.)

pass调用其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)
>>> print it.next()
0
>>> print it.next()
1
>>> print it.send(8)
8
>>> print it.next()
9
>>> print it.next()
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    print it.next()
StopIteration

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

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

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

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

如果您需要在发生GeneratorExit时运行清理代码,建议您使用try: ... finally:套件,而不要捕获GeneratorExit

这些变化的累积影响是使生成者从单向信息生产者转变为生产者和 Consumer。

生成器也成为“协程”(coroutines),子程序的一种更通用的形式。子例程在一个点进入,而在另一点退出(函数的顶部和一个return语句),但是协程可以在许多不同的点(yield语句)进入,退出和恢复。

Built-in functions

让我们更详细地查看迭代器经常使用的内置函数。

Python 的两个内置函数map()filter()已过时;它们复制了列表推导的Function,但是返回实际的列表而不是迭代器。

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()
>>> map(upper, ['sentence', 'fragment'])
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

如上所示,列表理解可以达到相同的效果。 itertools.imap()函数的作用相同,但可以处理无限迭代器;稍后将在itertools模块的部分中进行讨论。

filter(predicate, iter)返回一个列表,其中包含满足特定条件的所有序列元素,并且类似地由列表推导重复。谓词**是返回某些条件真值的函数;要与filter()一起使用,谓词必须采用单个值。

>>> def is_even(x):
...     return (x % 2) == 0
>>> filter(is_even, range(10))
[0, 2, 4, 6, 8]

这也可以写成列表理解:

>>> [x for x in range(10) if is_even(x)]
[0, 2, 4, 6, 8]

filter()itertools模块中有一个对应对象itertools.ifilter(),它返回一个迭代器,因此可以像itertools.imap()一样处理无限序列。

reduce(func, iter, [initial_value])itertools模块中没有对应项,因为reduce(func, iter, [initial_value])会对所有可迭代元素进行累积操作,因此无法应用于无限迭代。 func必须是一个接受两个元素并返回单个值的函数。 reduce()接受迭代器返回的前两个元素 A 和 B 并计算func(A, B)。然后,它请求第三个元素 C,计算func(func(A, B), C),将此结果与返回的第四个元素结合起来,并 continue 直到迭代器用完。如果 iterable 完全不返回任何值,则会引发TypeError异常。如果提供了初始值,则将其用作起点,并且func(initial_value, A)是第一个计算。

>>> import operator
>>> reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty sequence with no initial value
>>> reduce(operator.mul, [1,2,3], 1)
6
>>> reduce(operator.mul, [], 1)
1

如果您将operator.add()reduce()结合使用,则会将 iterable 的所有元素加起来。这种情况很常见,以至于有一个特殊的内置sum()来计算它:

>>> reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0

但是,对于reduce()的许多用法,可以编写明显的for循环会更清楚:

# Instead of:
product = reduce(operator.mul, [1,2,3], 1)

# You can write:
product = 1
for i in [1,2,3]:
    product *= i

enumerate(iter)对可迭代元素进行计数,返回包含计数和每个元素的 2Tuples。

>>> 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, [cmp=None], [key=None], [reverse=False])将可迭代的所有元素收集到一个列表中,对该列表进行排序,然后返回排序后的结果。 cmpkeyreverse参数传递给构造的列表的.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]

(有关排序的详细讨论,请参阅 Python Wiki https://wiki.python.org/moin/HowTo/Sorting上的 Sorting mini-HOWTO。)

内置的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

小函数和 lambda 表达式

在编写函数式程序时,通常需要很少的函数充当谓词或以某种方式组合元素。

如果有合适的 Python 内置函数或模块函数,则完全不需要定义新函数:

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

如果所需的Function不存在,则需要编写它。编写小函数的一种方法是使用lambda语句。 lambda接受多个参数和将这些参数组合在一起的表达式,并创建一个返回该表达式值的小函数:

lowercase = lambda x: x.lower()

print_assign = lambda name, value: name + '=' + str(value)

adder = lambda x, y: x+y

另一种选择是只使用def语句并以通常的方式定义一个函数:

def lowercase(x):
    return x.lower()

def print_assign(name, value):
    return name + '=' + str(value)

def adder(x,y):
    return x + y

哪种选择更可取?这是一个风格问题;我通常的做法是避免使用lambda

我偏爱的原因之一是lambda可以定义的Function非常有限。结果必须是可作为单个表达式计算的,这意味着您不能具有多向if... elif... else比较或try... except语句。如果您try在lambda语句中执行过多操作,则finally将得到一个过于复杂的表达式,难以理解。快速,以下代码在做什么?

total = reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

您可以弄清楚,但是需要花一些时间来解开表达式以弄清楚发生了什么。使用简短的嵌套def语句会使情况变得更好一些:

def combine (a, b):
    return 0, a[1] + b[1]

total = reduce(combine, items)[1]

但是如果我只使用了for循环,那将是最好的:

total = 0
for a, b in items:
    total += b

或内置的sum()和生成器表达式:

total = sum(b for a,b in items)

当写成for循环时,reduce()的许多用法会更清晰。

弗雷德里克·隆德(Fredrik Lundh)曾经建议使用以下规则来重构lambda的用法:

  • 编写一个 lambda 函数。

  • 写一个 Comments,解释 lambda 的作用。

  • 研究 Comment 一段时间,然后考虑一个名称,该名称可以体现 Comment 的本质。

  • 使用该名称将 lambda 转换为 def 语句。

  • 删除 Comment。

我非常喜欢这些规则,但是您可以自由地讨论这种无 lambda 样式是否更好。

itertools 模块

itertools模块包含许多常用的迭代器以及用于组合多个迭代器的函数。本节将pass显示一些小示例来介绍模块的内容。

该模块的Function分为几大类:

  • 基于现有迭代器创建新迭代器的函数。

  • 将迭代器元素视为函数参数的函数。

  • 选择迭代器输出部分的Function。

  • 用于对迭代器的输出进行分组的Function。

创建新的迭代器

itertools.count(n)返回无限的整数流,每次增加 1.您可以选择提供起始 Numbers,默认为 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)保存提供的可迭代内容的副本,并返回一个新的迭代器,该迭代器从头到尾返回其元素。新的迭代器将无限重复这些元素。

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

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.izip(iterA, iterB, ...)从每个可迭代对象中获取一个元素,并将其返回到 Tuples 中:

itertools.izip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

它类似于内置的zip()函数,但是不构造内存列表并在返回之前耗尽所有 Importing 迭代器。相反,只有在需要时才构造并返回 Tuples。 (此行为的技术术语是lazy evaluation。)

该迭代器旨在与长度相同的可迭代对象一起使用。如果可迭代项的长度不同,则所得流的长度将与最短可迭代项的长度相同。

itertools.izip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

但是,您应该避免这样做,因为可能会从较长的迭代器中取出一个元素并将其丢弃。这意味着您无法 continue 使用迭代器,因为您可能会跳过被丢弃的元素。

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, ...

元素上的调用Function

两个函数用于在可迭代的内容上调用其他函数。

itertools.imap(f, iterA, iterB, ...)返回包含f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...的流:

itertools.imap(operator.add, [5, 6, 5], [1, 2, 3]) =>
  6, 8, 8

operator模块包含一组与 Python 的运算符相对应的函数。一些示例是operator.add(a, b)(相加两个值),operator.ne(a, b)(与a!=b相同)和operator.attrgetter('id')(返回获取"id"属性的可调用对象)。

itertools.starmap(func, iter)假定 iterable 将返回 Tuples 流,并使用这些 Tuples 作为参数调用f()

itertools.starmap(os.path.join,
                  [('/usr', 'bin', 'java'), ('/bin', 'python'),
                   ('/usr', 'bin', 'perl'),('/usr', 'bin', 'ruby')])
=>
  /usr/bin/java, /bin/python, /usr/bin/perl, /usr/bin/ruby

Selecting elements

另一组函数根据谓词选择迭代器元素的子集。

itertools.ifilter(predicate, iter)返回谓词为其返回 true 的所有元素:

def is_even(x):
    return (x % 2) == 0

itertools.ifilter(is_even, itertools.count()) =>
  0, 2, 4, 6, 8, 10, 12, 14, ...

itertools.ifilterfalse(predicate, iter)相反,返回谓词返回 false 的所有元素:

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

itertools.takewhile(predicate, iter)返回元素的时间只要谓词返回 true。一旦谓词返回 false,迭代器将发出 signal 通知其结果结束。

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, ...

Grouping elements

我将讨论的最后一个函数itertools.groupby(iter, key_func=None)最复杂。 key_func(elem)是可以为 Iterable 返回的每个元素计算键值的函数。如果您不提供键Function,则键就是每个元素本身。

groupby()从底层可迭代对象收集具有相同键值的所有连续元素,并返回一个包含键值和具有该键的元素的迭代器的 2Tuples 流。

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state ((city, state)):
    return state

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模块包含一些高阶函数。 高阶函数 将一个或多个函数作为 Importing 并返回一个新函数。此模块中最有用的工具是functools.partial()函数。

对于以Function风格编写的程序,您有时会希望构造现有函数的变体,并在其中填充一些参数。考虑一下 Python 函数f(a, b, c);您可能希望创建一个等效于f(1, b, c)的新函数g(b, c);您正在为f()的参数之一填充值。这称为“部分Function应用程序”。

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

操作员模块

前面提到了operator模块。它包含一组与 Python 运算符相对应的函数。这些函数在函数式代码中通常很有用,因为它们使您不必编写执行单个操作的琐碎函数。

该模块中的一些Function是:

  • math 运算:add()sub()mul()div()floordiv()abs()

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

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

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

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

有关完整列表,请查阅操作员模块的文档。

修订历史和致谢

作者要感谢以下人员为本文的各种草案提供建议,更正和帮助: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模块上添加了一部分;在操作员模块上添加简短的部分;其他一些编辑。

References

General

《计算机程序的结构和解释》,由 Harold Abelson 和 Gerald Jay Sussman 与 Julie Sussman 共同撰写。全文位于https://mitpress.mit.edu/sicp/。在这本经典的计算机科学教科书中,第 2 章和第 3 章讨论了使用序列和流来组织程序内部的数据流。本书将 Scheme 用作示例,但是这些章节中描述的许多设计方法都适用于Function样式的 Python 代码。

http://www.defmacro.org/ramblings/fp.html:使用 Java 示例的函数式编程的一般介绍,并且具有悠久的历史介绍。

https://en.wikipedia.org/wiki/Functional_programming:描述Function编程的通用 Wikipedia 条目。

https://en.wikipedia.org/wiki/Coroutine:协程条目。

https://en.wikipedia.org/wiki/Currying:关于“ currying”(概念)的条目。

Python-specific

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

Mertz 还为 IBM 的 DeveloperWorks 网站撰写了三部分的函数式编程文章。看到

part 1part 2part 3

Python documentation

itertools模块的文档。

operator模块的文档。

PEP 289:“生成器表达式”

PEP 342:“pass增强的生成器进行协程”描述了 Python 2.5 中的新生成器Function。