简介
在前面的一篇文章里,我简单的介绍了一下python iterator的一些基本定义和使用思路。从表面上来看,iterator只是迭代的去访问一组内容,在实际使用的过程中如果结合一些其他的手法,能够用一种很简练的方式实现一些很强大的功能。这里一并做一个总结。
使用总结
Iterator的基本使用方法
我们知道,常用的iterator使用方法有直接的for循环。而从前面的文章里也看到,在某些情况下,因为iterator里面有定义了__next__()方法,我们可以调用next()方法来访问它。
比如说我们有一个文件test.txt,我们需要读出来里面的内容。一个最常用的方法如下:
with open('test.txt') as f:
... for line in f:
... print(line, end='')
我们也可以采用对应的next()方法,对应实现的代码如下:
with open('test.txt') as f:
... try:
... while True:
... line = next(f)
... print(line, end='')
... except StopIteration:
... pass
我们知道,整因为iterator内部的实现是通过StopIteration这个异常来保证运行的结束,所以这里才需要捕捉这个异常来作为访问结束的退出。和前面的方法比起来,这个显得稍微臃肿了一点。当然,我们可以对这些代码稍微改进一点,毕竟只是一个判断迭代器是否走完,用异常显得太笨重。改进后的代码如下:
with open('test.txt') as f:
while True:
line = next(f, None)
if line is None:
break
print(line, end='')
这里的一个区别就是next()方法多带了一个参数None, 这里表示如果后面读取不到内容了,则返回None,这样我们就不用再去通过捕捉异常的方式来判断,只要判断一下读取到的内容是否为None就可以了。
这几种方式的比较,第一种显然要简单一些。只是在某些情况下如果我们没法用for循环去访问迭代器的时候,可以考虑用后面这种方式。
反向移动Iterator
在通常情况下,我们迭代器都是从头到尾的去遍历元素集合,但是在某些特殊的情况下,比如说我们需要从集合的末位遍历到开头,那该怎么来使用iterator呢?一个简单的办法就是使用reversed()方法。最常用的方式在于对一个list操作:
>>> a = [1, 2, 3, 4]
>>> for x in reversed(a):
... print(x)
...
4
3
2
1
在前面访问文件的示例中,如果我们也希望达到这样的效果,则需要将文件内容先读进内存,然后按照相反的顺序输出,这种实现的方式如下:
f = open('test.txt')
for line in reversed(list(f)):
print(line, end='')
这种方式有一个潜在的问题,就是对于比较小的文件是可行的,对于比较大的文件,如果不可能将所有内容都装载到内存中的话,这种方法就不可行了。需要通过f.seek()先找到文件的末位,然后再倒过来一步步的往前读。
我们知道,对于一个对象来说,如果它实现了 __iter__方法,相当于实现了一个迭代器的返回定义方法。所以我们可以通过iter()来取得这个对象的迭代器。然后我们再通过循环来遍历它。对于反向遍历,我们也可以定义类似的方法实现:
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1
这里相当于定义了两个迭代器,一个是正向的,一个是反向的。我们可以这样来使用它们:
>>> for n in iter(c):
... print(n)
...
10
9
8
7
6
5
4
3
2
1
>>> for n in reversed(c):
... print(n)
...
1
2
3
4
5
6
7
8
9
10
Iterator数据切片
有的时候我们希望取迭代器数据中间的一部分。一种典型的思路就是我用一个计数器,专门计算取到那个范围的再统一返回。实际上这里有一种比较简单的手法。就是使用itertools.islice()方法。以前面我们读取文件的代码为例:
>>> with open('test.txt') as f:
... import itertools
... for x in itertools.islice(f, 2, 4):
... print(x)
...
ghi
jkl
这里实际返回的是第3行和第4行的内容。实际上itertools.islice()方法通过指定的参数(2, 4)确定了取的值范围为第3,4行。然后通过丢弃前面的行,直到进入到我们所要求的数据范围。这样就省略了很多我们自己来写一个计数器跟踪它们的代码。islice()方法可以起到一个很好的数据范围过滤效果。如果我们设定itertools.islice(f, 2, None),则相当于取数组f[2:]的效果。
Iterator内容过滤
我们在遍历一些集合的时候,希望过滤掉一部分元素。这个时候,一个典型的方法就是我们所想到的list comprehension,比如说,我们要输出一个文件的内容,但是要跳过里面所有加了#号的注释部分。我们可以这样来做:
>>> with open('test.txt') as f:
... lines = (line for line in f if not line.startswith('#'))
... for line in lines:
... print(line, end='')
如果有的时候我们只是为了跳过文件开头的那部分注释呢?还有一个更加简单的办法,就是利用itertools里的dropwhile方法。这种用法如下:
>>> from itertools import dropwhile
>>> with open('test.txt') as f:
... for line in dropwhile(lambda line: line.startswith('#'), f):
... print(line, end='')
同时迭代多个序列
有时候我们希望能够同时遍历多个序列,比如有序列a = [1, 2, 3, 4, 5], b = ['a', 'b', 'c', 'd', 'e'],我们如果要同时遍历的话,可以采用如下的方式:
>>> xpts = [1, 5, 4, 2, 10, 7]
>>> ypts = [101, 78, 37, 15, 62, 99]
>>> for x, y in zip(xpts, ypts):
... print(x, y)
...
1 101
5 78
4 37
2 15
10 62
7 99
因为使用了zip()方法,我们将两个集合里的内容都同时取出来,按照tuple的方式一个个的组织起来。所以我们访问的时候也是通过一个个tuple的方式来读取。这里我们提供的两个list是长度一致的,如果不一致会怎么样呢?我们再来试试另外两个序列:
>>> a = [1, 2, 3, 4]
>>> b = ['a', 'b', 'c']
>>> for x, y in zip(a, b):
... print(x, y)
...
1 a
2 b
3 c
从代码运行的结果来看,默认是遍历到短的那个序列结束。如果我们需要到那个长的序列结束呢?这里有另外一种办法:
>>> from itertools import zip_longest
>>> for i in zip_longest(a, b):
... print(i)
...
(1, 'a')
(2, 'b')
(3, 'c')
(4, None)
这里引用了zip_longest方法,它可以将两个序列组合起来,不过对于短的那个序列,用None来补齐。
将几个序列串在一起
我们可以直接看如下的代码:
>>> from itertools import chain
>>> a = [1, 2, 3, 4]
>>> b = ['a', 'b', 'c']
>>> for x in chain(a, b):
... print(x)
...
1
2
3
4
a
b
c
一个chain方法就解决了大部分问题了。和我们默认想到的方法比起来,chain方法效率更加高。因为我们最开始会考虑将两个或者多个序列连在一起,比如a + b,这样会创造一个新的序列出来,这样带来的成本开销明显偏大了。
将嵌套的序列变平
这是一个有意思的问题,因为一般来说当我们需要访问一个数组的时候,比如说a = [1, 2, [3, 4, [5, 6], 7, 8], 9, 10],我们希望能够将他们所有的元素都输出,并使得他们看起来像就是一个一维数组那样,如a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]。我们默认的思路该怎么办呢?只怕一开始就是碰到一个元素的时候会判断它是否为数组,如果是的则递归的去输出它的元素。
Python里面有一个很强大的特性可以很好的实现这个方法:
from collections import Iterable
def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
yield from flatten(x)
else:
yield x
这种实现里面有一个额外的ignore_types,里面列举了一些类型我们可以不需要进一步的去遍历。比如说str,我们一般碰到一个字符串可以直接将他们作为一个整的对象输出而不是再对它们进一步拆分的遍历。最有意思的地方在yield from这个部分。yield from这个部分的意思是将后续的值作为它本身的一个subroutine。所以它们就会被当作一个拉平的数组。关于yield from这部分我们在后面的文章中会专门讲述。
按照这个方式,我们使用它们的代码如下:
>>> from nested import flatten
>>> items = [1, 2, [3, 4, [5, 6], 7], 8]
>>> for x in flatten(items):
... print(x)
...
1
2
3
4
5
6
7
8
迭代多个有序排列数组
这个问题不太好用一句话描述,就是说假定我们有若干个已经排序的数组了。当我们希望能够去遍历这所有的序列,但是保证我们每次都取出他们中间最小的元素,保证所有输出还是一个严格排序的结果,我们该怎么办呢?实际上,这是一个多路归并排序的问题。在前面的一些文章里有过讨论,不过要做一个好的java实现我们可是费了一番功夫。这里有什么好的招呢?
>>> import heapq
>>> a = [1, 4, 7, 10]
>>> b = [2, 5, 6, 11]
>>> for c in heapq.merge(a, b):
... print(c)
...
1
2
4
5
6
7
10
11
这里是归并两路的数据结果。在一些我们如果要归并多个文件的情况下,也可以这样来做。因为这里heapq.merge不是一次将所有的数据都装载到内存里,它只是每次取很小的一部分,像generator一样。所以对于大文件的合并用这种方式来做。呵呵,寥寥几行代码就解决了问题,不能不说,很好很强大啊。
总结
Iterator的定义方法虽然看起来很简单,但是它的使用也可以非常的复杂和灵活。通过结合一些库的支持,我们可以实现非常强大的计算效果。当然,前提是我们需要知道去哪里找到这些库和知道这些用法。
参考材料
python cookbook