【Python Cookbook】迭代器与生成器(二)
迭代器与生成器(二)
- 5.反向迭代
- 6.带有外部状态的生成器函数
- 7.迭代器切片
- 8.跳过可迭代对象的开始部分
5.反向迭代
你想反方向迭代一个序列。
使用内置的 reversed()
函数,比如:
>>> a = [1, 2, 3, 4]
>>> for x in reversed(a):
... print(x)
...
4
3
2
1
反向迭代仅仅当对象的大小可预先确定或者对象实现了 __reversed__()
的特殊方法时才能生效。如果两者都不符合,那你必须先将对象转换为一个列表才行,比如:
# Print a file backwards
f = open('somefile')
for line in reversed(list(f)):print(line, end='')
要注意的是如果可迭代对象元素很多的话,将其预先转换为一个列表要消耗大量的内存。
很多程序员并不知道可以通过在自定义类上实现 __reversed__()
方法来实现反向迭代。比如:
class Countdown:def __init__(self, start):self.start = start# Forward iteratordef __iter__(self):n = self.startwhile n > 0:yield nn -= 1# Reverse iteratordef __reversed__(self):n = 1while n <= self.start:yield nn += 1for rr in reversed(Countdown(30)):print(rr)
for rr in Countdown(30):print(rr)
定义一个反向迭代器可以使得代码非常的高效,因为它不再需要将数据填充到一个列表中然后再去反向迭代这个列表。
6.带有外部状态的生成器函数
你想定义一个生成器函数,但是它会调用某个你想暴露给用户使用的外部状态值。
如果你想让你的生成器暴露外部状态给用户,别忘了你可以简单的将它实现为一个类,然后把生成器函数放到 __iter__()
方法中过去。比如:
from collections import dequeclass linehistory: # 定义了一个名为 linehistory 的类,用于在迭代文件行时跟踪历史记录。def __init__(self, lines, histlen=3):self.lines = linesself.history = deque(maxlen=histlen) # self.history 是一个双端队列,最多存储 histlen 个最近访问的行及其行号。def __iter__(self):for lineno, line in enumerate(self.lines, 1): # 使用 enumerate 遍历 lines,从1开始计数行号。self.history.append((lineno, line)) # 每次迭代时,将当前行号和行作为元组 (lineno, line) 添加到 history 队列。yield line # 返回当前行,实现惰性迭代。def clear(self):self.history.clear() # 清空历史记录队列。
🚀 当你需要编写一个生成器函数,但这个生成器还需要提供一些额外的状态(比如历史记录、计数器等)给外部使用时,单纯用生成器函数可能不够方便(因为生成器函数的主要目的是
yield
值,而不是管理状态)。这时候,你可以改用类来实现,将生成器逻辑放在__iter__()
方法中,并通过类的属性暴露外部状态。
为了使用这个类,你可以将它当做是一个普通的生成器函数。然而,由于可以创建一个实例对象,于是你可以访问内部属性值,比如 history
属性或者是 clear()
方法。代码示例如下:
with open('somefile.txt') as f:lines = linehistory(f)for line in lines:if 'python' in line:for lineno, hline in lines.history:print('{}:{}'.format(lineno, hline), end='')
- 打开文件
somefile.txt
,创建linehistory
实例lines
。- 迭代文件每一行:
- 如果行包含
python
,打印最近histlen
(默认为 3)行的历史记录(行号和内容)。end=''
避免打印多余的换行符(因为文件行本身已包含换行符)。
关于生成器,很容易掉进函数无所不能的陷阱。如果生成器函数需要跟你的程序其他部分打交道的话(比如暴露属性值,允许通过方法调用来控制等等),可能会导致你的代码异常的复杂。如果是这种情况的话,可以考虑使用上面介绍的定义类的方式。在 __iter__()
方法中定义你的生成器不会改变你任何的算法逻辑。由于它是类的一部分,所以允许你定义各种属性和方法来供用户使用。
一个需要注意的小地方是,如果你在迭代操作时不使用 for
循环语句,那么你得先调用 iter()
函数。比如:
>>> f = open('somefile.txt')
>>> lines = linehistory(f)
>>> next(lines)
Traceback (most recent call last):File "<stdin>", line 1, in <module>
TypeError: 'linehistory' object is not an iterator>>> # Call iter() first, then start iterating
>>> it = iter(lines)
>>> next(it)
'hello world\n'
>>> next(it)
'this is a test\n'
>>>
7.迭代器切片
你想得到一个由迭代器生成的切片对象,但是标准切片操作并不能做到。
函数 itertools.islice()
正好适用于在迭代器和生成器上做切片操作。比如:
>>> def count(n):
... while True:
... yield n
... n += 1
...
>>> c = count(0)
>>> c[10:20]
Traceback (most recent call last):File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20):
... print(x)
...
10
11
12
13
14
15
16
17
18
19
>>>
迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道(并且也没有实现索引)。函数 islice()
返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。然后才开始一个个的返回元素,并直到切片结束索引位置。
这里要着重强调的一点是 islice()
会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。所以如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
8.跳过可迭代对象的开始部分
你想遍历一个可迭代对象,但是它开始的某些元素你并不感兴趣,想跳过它们。
itertools
模块中有一些函数可以完成这个任务。首先介绍的是 itertools.dropwhile()
函数。使用时,你给它传递一个函数对象和一个可迭代对象。它会返回一个迭代器对象,丢弃原有序列中直到函数返回 False
之前的所有元素,然后返回后面所有元素。
为了演示,假定你在读取一个开始部分是几行注释的源文件。比如:
>>> with open('/etc/passwd') as f:
... for line in f:
... print(line, end='')
...
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode. At other times, this information is provided by
# Open Directory.
...
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>
如果你想跳过开始部分的注释行的话,可以这样做:
>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
... for line in dropwhile(lambda line: line.startswith('#'), f):
... print(line, end='')
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>
这个例子是基于根据某个测试函数跳过开始的元素。如果你已经明确知道了要跳过的元素的序号的话,那么可以使用 itertools.islice()
来代替。比如:
>>> from itertools import islice
>>> items = ['a', 'b', 'c', 1, 4, 10, 15]
>>> for x in islice(items, 3, None):
... print(x)
...
4
10
15
>>>
在这个例子中, islice()
函数最后那个 None
参数指定了你要跳过前面 3 个元素,获取第 4 个到最后的所有元素, 如果 None
和 3 的位置对调,意思就是仅仅获取前三个元素恰恰相反(这个跟切片的相反操作 [3:]
和 [:3]
原理是一样的)。
函数 dropwhile()
和 islice()
其实就是两个帮助函数,为的就是避免写出下面这种冗余代码:
with open('/etc/passwd') as f:# Skip over initial commentswhile True:line = next(f, '')if not line.startswith('#'):break# Process remaining lineswhile line:# Replace with useful processingprint(line, end='')line = next(f, None)
跳过一个可迭代对象的开始部分跟通常的过滤是不同的。比如,上述代码的第一个部分可能会这样重写:
with open('/etc/passwd') as f:lines = (line for line in f if not line.startswith('#'))for line in lines:print(line, end='')
这样写确实可以跳过开始部分的注释行,但是同样也会跳过文件中其他所有的注释行。换句话讲,我们的解决方案是仅仅跳过开始部分满足测试条件的行,在那以后,所有的元素不再进行测试和过滤了。
最后需要着重强调的一点是,本节的方案适用于所有可迭代对象,包括那些事先不能确定大小的,比如生成器,文件及其类似的对象。