Python函数式编程

本文翻译自Functional Programming Howto

lambda

本文将介绍Python中函数式编程的特性。在对函数式编程的概念有了了解后,本文会介绍iterators和generators等语言特性,还有itertoolsfunctools等相关的库。

函数式编程

本章节将会介绍函数式编程的一些基本概念;如果只是对Python的语言特性感兴趣的话,可以跳过。

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

  • 大多数的编程语言是面向过程的:程序是计算机处理输入的指令集合。C, Pascal, 甚至Unix shell都是这类。
  • 在命令式编程中,用户告诉计算机需要做什么,语言的实现来完成高效的计算。SQL可能是最广为人知的宣告式編程语言了;SQL语句负责查询描述要获取的数据集合,SQL引擎决定是扫描表或者使用索引,首先执行哪些字查询等等问题。
  • 面向对象的程序操作对象的集合。对象具有内部状态,并支持查询和修改内部状态的方法。Smalltalk和Java是面向对象编程语言。C++和Python支持面向对象,但是不强制使用面向对象特性。
  • 函数式编程语言将问题分解成一系列函数。理想情况下,函数接受输入产生输出,并且没有影响这一过程的内部状态。众所周知的函数式语言包括ML系列(标准ML,OCaml以及其他变种)和Haskell。

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

在一个函数式的程序中,输入会流经一组函数。每个函数对自己的输入进行处理并产出输出。对于那些有修改内部状态副作用和在进行在返回值中不可见的修改的函数,函数式编程是不鼓励的。没有副作用的函数被称之为纯函数。没有副作用意味着不使用随程序运行而更新的数据结构;每个函数的输出只取决于它的输入。

有些编程语言对于函数是否是纯函数有着严格的限制,它们甚至没有类似a=3或者c = a + b这样的赋值语句,但是想要完全避免副作用是很困难的。比如打印到屏幕或者写入磁盘文件就是有副作用的。比如在Python中,printtime.sleep(1)都没有返回有用的值;它们被调用只是为了发送文本到屏幕或者暂停执行一秒的副作用。

函数式风格的Python程序通常不会走向避免所有I/O操作或所有赋值操作的极端;它们一般会提供函数式的接口,然后内部使用非函数式的特性实现功能。比如,一个函数的内部依然会对局部变量赋值,但是不会修改全局变量或者有其他的副作用。

函数式编程可以看作是面向对象编程的对立面。对象包含了一些内部状态和修改这些状态的方法,面向对象的程序指定对象的正确状态。而函数式编程希望尽量避免状态的改变,在函数之间处理数据流。在Python中,你可以通过编程接收和返回对象来同时利用这两种编程方式,对象和应用有关(e-mail,事务等等)。

函数式的设计似乎是一个奇怪的制约因素。为什么要避免对象和副作用呢?因为函数式风格有以下理论和实践的优势。

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

正式证明性

使用函数式编程的一个理论上的好处是,在数学上验证一个程序的正确性是比较容易的。

很长一段时间以来,研究人员一直在寻找通过数学证明程序正确的方法。这种证明的方法和通过输入测试数据验证输出的正确性,以及通过读取程序的源代码来判断正确性的方法不同;它想要严格证明程序对所有可能的输入都能产生正确的结果。

用于证明程序正确的技术是,记下不变量,输入数据和一直为真的程序变量。对每一行代码和不变量X,Y,可以知道运行前X和Y是否为真,和执行后X'和Y'是否为真。进行这样的比较直到程序结束,这是不变量应该符合程序输出所需的条件。

赋值行为会修改之前为真的不变量,而不会产生新的可以向后传递的不变量,因此上面的技术碰到赋值行为的时候会难以继续下去,这也导致了函数式风格会避免赋值。

不幸的是,证明程序正确无疑是不切实际的,这和Python无关。即使是极为简单的程序也需要几页长的证明;中等程度复杂程序的正确性证明难度将是巨大的,日常使用的程序(Python解释器,XML解析器,Web浏览器)几乎没有可以被证明是正确的。即使是生成了一个证明,也会存在需要验证这个证明的问题;如果这个证明有问题,那么得到程序正确的结论将是错误的。

模块化

函数式编程一个更实际的好处是它会强制使用者将问题分解。程序因此会更加模块化。相比一个实现复杂变换的长函数,实现完成一件事的小函数更加容易。短小的函数更易于阅读和检查错误。

易于测试和调试

函数式风格的程序测试和调试起来更加容易。

函数通畅很小并且功能明确,所以调试起来会很方便。当程序无法运行时,可以在每个函数入口检查数据是否正确。可以通过查看中间输入和输出,来快速定位出现bug的函数。

每个函数都可以成为单元测试的目标,因此测试也会容易些。函数不依赖于系统状态,因此测试只需要合成正确的输入,然后检查输出是否符合预期。

组合性

在编写函数式风格的程序时,会编写许多具有不同输入和输出的函数。这些函数有些是专门针对特定的应用,但是其他的函数将在各种不同的程序中有用。比如,一个传入目录路径并返回目录中所有XML文件的函数,或者一个传入文件名并返回其内容的函数,是可以在多种不同的情况下使用的。

随着时间推移,你可以慢慢组建属于自己的库。通常,你可以使用已有的函数,改变配置,然后实现一些专门针对当前任务的函数来组成新的程序。

Python特性

迭代器

我将首先介绍Python一个语言特性,这个特性是编写函数式风格程序的基础:迭代器。

迭代器是表示数据流的对象;此对象每次返回数据的一个元素。一个Python迭代器必须实现next()方法,该方法不接受参数,并且总是返回数据流的下一个元素。如果流中没有元素,next()必须抛出StopIteration异常。迭代器不必是有限的;编写产生无限数据流的迭代器非常合理。

内置的iter()函数接受任意对象,并尝试返回一个返回对象元素的迭代器,如果对象不支持迭代,则抛出TypeError异常。Python内置的数据类型中有几种是支持迭代的,最常见的有列表和字典。如果一个对象可以生成一个迭代器,则称该对象是可迭代的。

可以体验一下迭代接口:

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> print it
<listiterator object at 0x100bc3950>
>>> 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()构造方法操作迭代器得到列表或者是元组。

>>> 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返回true。如果迭代器是无限的话,明显会有一些问题;max()min()将永远不会返回,如果X没有出现在流中,innot 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]
...
Feb 2
Aug 8
Jan 1
Dec 12
Oct 10
Mar 3
Sep 9
May 5
Jun 6
Jul 7
Apr 4
Nov 11

请注意,上面输出的顺序是随机的,因为排序是基于字典对象的哈希顺序。

对字典对象使用iter()方法会返回键的迭代器,但是字典有其他方法得到不同的迭代器。如果想迭代键,值,键/值对,分别可以调用iterkeys(),itervalues(),iteritems()来的得到对应的迭代器。

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

可以从一个可迭代对象生成一个集合,并迭代器中的元素

set(['Italy', 'US', 'France'])
>>> S = set((2, 3, 5, 7, 11, 13))
>>> for i in S:
...     print i
...
2
3
5
7
11
13

生成器表达式和列表推导

对迭代器通常有两个操作:

  1. 对每个元素进行操作
  2. 选择满足某个条件的元素的子集。
    比如给定一个字符串的列表,可能需要去除每个字符串末尾的空格或者提取包含给定子串的字符串。

列表推导和生成器表达式是这种操作的简写符号,这是从Haskell中借鉴来的。可以通过下面的代码从字符串流中去掉所有空格:

line_list = ['  line 1\n', 'line 2  \n', ...]

# 生成器表达式,返回迭代器
stripped_iter = (line.strip() for line in line_list)

# 列表推导式,返回列表
stripped_list = [line.strip() for line in line_list]

可以通过添加"if"条件来筛选特定的元素。

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

使用列表推导式可以得到一个Python列表;strip_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子语句都是可选的;只有当条件为true时,表达式才被计算并添加到结果中。

生成器表达式必须写在括号内,也可以写在表示函数调用的括号内。如果将要创建的迭代器马上会传给一个函数,可以这样写:

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   # 跳过这个元素
    for expr2 in sequence2:
        if not (condition2):
            continue   # 跳过这个元素
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # 跳过这个元素

            # 输出表达式的值

这意味着,当有多个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用来生成一个元组,则必须将其用()括起来。下面例子中第一个有语法错误,第二个是正确的。

# 语法错误
[ x,y for x in seq1 for y in seq2]
# 正确
[ (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 0x10dfe2a50>
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

写成for i in generate_ints(3)或者a,b,c=generate_ints(3)也是一样的。

在生成器函数中,return语句只能在没有值的情况下使用,并且表示值的结束。在执行return语句后,生成器不能再返回更多的值。如果在生成器函数中return带了返回值,比如return 5,会被认为是语法错误。可以通过手动抛出StopIteration异常,或者让函数执行到最后,来让生成器函数不再产生新的值。
可以通过自定义的类,将生成器的所有本地变量存储为实例,来达到生成器的效果。比如,通过将self.count设置为0,将self.next()实现为自增self.count并返回,来返回一列整数。然而,对于一个有一定复杂度的生成器,实现自定义的类会更麻烦。

Python测试套件中的test_generator.py有更多有意思的例子。下面是一个递归实现树的顺序遍历的生成器。

def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        
        yield t.label
        
        for x in inorder(t.right):
            yield x

另外两个例子提供了N皇后问题(在N*N棋盘上放N个皇后使其彼此不会互相威胁)和骑士之旅的解法。

向生成器传值

在Python2.4及之前的版本中,生成器只能产生输出。一旦生成器的代码被调用生成一个迭代器,当函数恢复执行时,没有办法将任何新的信息传递到函数中。可以通过让生成器检查全局变量,或者传递一些可变对象供调用方修改,来将新信息传入,但是这样很不优雅。

在Python2.5中,有一个简单的方法将值传递给生成器。yield成为了表达式,返回一个可以分配给变量或者以其他方式运行的值。

val = (yield i)

我推荐大家在执行和返回值相关的操作时,始终将yield表达式括起来,如上面的例子。这个括号并不总是必须的,但是总是添加它比记住何时需要它们更容易。

通过调用send(value)方法,可以将值传到生成器中。这个方法继续执行生成器的代码,yield表达式会返回传入的值。如果next()方法被调用,yield返回None

下面是一个每次增加1的简单计数器,允许修改内部计数器的值。

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # 如果给定value,改变计数器
        if val is not None:
            i = val
        else:
            i += 1

下面是改变计数器的方法:

>>> it = counter(10)
>>> print it.next()
0
>>> print it.next()
1
>>> it.send(8)
8
>>> print it.next()
9
>>> print it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

因为yield会经常返回None,代码中应该检查这种情况。不要在表达式中使用这个值,除非确定send()是恢复函数的唯一方法。

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

  • throw(type, value=None, traceback=None)用于在生成器中抛出异常;当生成器暂停执行时,由yield语句抛出这个异常。
  • close()通过在生成器中抛出GeneratorExit异常来终止迭代。在抛出这个异常后,生成器代码必须抛出GeneratorExit或者StopIteration;捕获这个异常是非法的,会触发RuntimeError。在Python的垃圾回收器对生成器进行回收时也会调用close()

处理GeneratorExit异常推荐使用try: ... finally:而不是catch 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()
>>> map(upper, ['sentence', 'fragment'])
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

正如上面所展示的,使用列表推导式可以实现一样的效果。itertools.imap()完成一样的功能,不过它可以处理无尽的迭代器;等会在讨论itertools模块的再谈。

filter(predicate, iter)返回一个包含满足特定条件的所有序列元素的列表,这个功能和列表推导重复。predicate是返回一些条件的真值的函数;为了和filter()一起使用,predicate只能传入一个参数。

>>> 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模块中没有对应的方法,因为它累积地对可迭代对象的所有元素执行操作,因此不能用于无限迭代。func函数必须接受两个参数并返回一个值。reduce()接受迭代器返回的前两个元素A和B,返回func(A, B)。之后请求第三个元素C,计算func(func(A, B), C),然后请求迭代器返回的第四个元素,持续这样的步骤一直到迭代完所有元素。如果可迭代对象不返回任何元素,会抛出TypeError异常。如果提供了初始值,第一轮运算会是func(initial_value, A)

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

如果在reduce()中使用operator.add(),会得到迭代对象所有值的和。这个场景使用非常广泛,有内置的sum()方法来计算。

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

在很多reduce()的使用场景中,使用for循环会更好。

product = reduce(operator.mul, [1,2,3], 1)

# 等同于
product = 1
for i in [1,2,3]:
    product *= i

enumerate(iter)对可迭代对象中的元素进行计数,返回计数值和没有元素的二元组。

>>> 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])将可迭代对象中的所有元素收集到列表中,对列表进行排序,并返回排序结果。cmp,keyreverse参数传递给构造的列表的.sort()方法。

>>> import random
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[3027, 8533, 16, 6602, 4183, 9577, 4842, 5713]
>>> sorted(rand_list)
[16, 3027, 4183, 4842, 5713, 6602, 8533, 9577]
>>> sorted(rand_list, reverse=True)
[9577, 8533, 6602, 5713, 4842, 4183, 3027, 16]

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

小函数和lambda表达式

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

如果有一个Python内置函数或是模块是合适的,那么就不需要重新定义一个新的功能:

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

如果不存在现有的函数,那么你需要实现一个。编写小函数的一种方法是使用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在可以定义的功能上是非常有限的。结果必须作为单个表达式计算,这意味着不能使用if... elif... elsetry... except语句。如果试图在lambda语句中做太多的事情,那么最终会出现一个难以理解的过于复杂的表达式。能快速说出下面代码的作用吗?

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重构的规则:

  1. 写一个lambda函数。
  2. 写一个注释,说明该lambda函数的作用。
  3. 研究注释一段时间,并想出一个名字来捕捉评论的本职。
  4. 使用该名称将lambda函数转换为def语句。
  5. 移除注释。

我很喜欢这些规则,但是你可以有自己的选择。

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)保存可迭代对象内容的副本,返回一个新的迭代器,该迭代器从头到尾返回其元素。新的迭代器将无限重复这些元素。

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, ...
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.izip(iterA, iterB, ...)每次从可迭代对象中读取一个元素,然后在元组中返回它们:

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

这个和内置的zip()函数类似,但是不构成内存列表,并在返回之前耗尽所有的输入迭代器;只有当它们被请求时,才构造并返回元组。(专业术语叫做惰性求值)。

该迭代器旨在于所有长度相同的可迭代对象使用。如果可迭代对象长度不同,生成的流将与最短的可迭代对象长度相同。

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

但是应该避免这样做,因为从更长的可迭代对象中读取的元素可能会被丢弃。这意味着不能进一步使用该可迭代对象,因为有跳过丢弃元素的风险。

itertools.islice(iter, [start], stop, [step])返回迭代器的切片流。它会在遇到第一个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

iterA ->
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
iterB ->
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

对可迭代对象的元素进行操作

有两个函数用于对可迭代的内容进行其他函数的调用。

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)假定可迭代对象返回一个元组流,并使用这些元组作为参数调用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

选择元素

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

itertools.ifilter(predicate, iter)返回所有predicate为真的元素:

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)正好相反,返回所有为假的元素:

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

itertools.takewhile(predicate, iter)只要predicate为真,就一直返回值。一旦predicate为假,就停止迭代。

def less_than_10(x):
    return (x < 10)
    
itertools.takewhile(less_than_10, itertoosl.count()) =>
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
    0

itertools.dropwhile(predicate, iter)丢弃所有predicate为真的值,返回其他值。

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.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 state
    
itertools.groupby(city_list, get_state) =>
    ('AL', iterator-1),
    ('AK', iterator-2),
    ('AZ', iterator-3), ...
    
iterator-1 =>
    ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
    ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
    ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

函数groupby()假定底层可迭代对象的内容已经根据键值进行排序。请注意,返回的迭代器依然使用底层的可迭代对象,因此必须在消费iterator-1中的内容后,再去请求iterator-2及其相应的键值。

functools模块

在Python2.5中引入的functools模块包含一些高阶函数。高阶函数将一个或多个函数作为输入,并返回一个新的函数。其中最有用的就是functools.partial()函数。

对函数式风格的程序来说,有时要构建具有填充一些参数的现有函数的变体。比如函数f(a, b, c);你可能希望创建一个新的函数g(b, c),相当于f(1, b, c);这被称之为“部分功能应用程序”。

partial的构造函数使用参数(function, arg1, arg2, ... kwarg1=value1, kwarg2=value2)。生成的对象是可调用的,所以可以用它来构造可以填充参数的函数。

下面是一个小例子:

import functools

def log(message, subsystem):
    # 向特定的system写入消息
    print '%s: %s' % (subsystem, message)
    ...
    
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

operator模块

之前提到过operator模块。它包含一组于Python操作符相对应的函数。这些函数在函数式风格的代码中通常很有用,因为它可以替代一些只包括单个操作的函数。

其中的一些函数有:

  • 数学运算:add(),sub(),mul(),div(),floordiv(),abs(),...
  • 逻辑运算:not_(),truth()
  • 位运算:and_(),or_(),invert()
  • 对比:eq(),ne(),lt(),le(),gt()ge()
  • 对象标识:is_(),is_not()

请参阅operator模块的文档来获取完整列表。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容