手动遍历迭代器
问题
你想遍历一个可迭代对象中的所有元素,但是却不想使用for循环
解决方案
def manual_iter():
with open('test.txt') as f:
try:
while True:
line = next(f)
print(line, end='')
except StopIteration:
pass
通常来讲, StopIteration
用来指示迭代的结尾。 然而,如果你手动使用上面演示的 next()
函数的话,你还可以通过返回一个指定值来标记结尾,比如 None
。 下面是示例:
with open('test.txt') as f:
while True:
line = next(f, None)
if line is None:
break
print(line, end='')
大多数情况下,我们会使用 for
循环语句用来遍历一个可迭代对象。 但是,偶尔也需要对迭代做更加精确的控制,这时候了解底层迭代机制就显得尤为重要了。
下面的交互示例向我们演示了迭代期间所发生的基本细节:
>>> items = [1, 2, 3]
>>> # Get the iterator
>>> it = iter(items) # Invokes items.__iter__()
>>> # Run the iterator
>>> next(it) # Invokes it.__next__()
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
代理迭代
问题
你构建了一个自定义容器对象,里面包含有列表、元组或其他可迭代对象。 你想直接在你的这个新容器对象上执行迭代操作。
解决方案
实际上你只需要定义一个 __iter__()
方法,将迭代操作代理到容器内部的对象上去。比如:
class Node(object):
def __init__(self,item):
self.__value = item
self.__root = []
def append_child(self,item):
self.__root.append(Node(item))
def __repr__(self):
return 'Node(%s)'%self.__value
def __iter__(self):
return iter(self.__root) #__iter__() 方法只是简单的将迭代请求传递给内部的 __root 属性
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
child3 = Node(3)
root.append_child(child1)
root.append_child(child2)
root.append_child(child3)
for ch in root:
print(ch)
讨论:
Python的迭代器协议需要 __iter__()
方法返回一个实现了 __next__()
方法的迭代器对象。 如果你只是迭代遍历其他容器的内容,你无须担心底层是怎样实现的。你所要做的只是传递迭代请求既可。
这里的 iter()
函数的使用简化了代码, iter(s)
只是简单的通过调用 s.__iter__()
方法来返回对应的迭代器对象, 就跟 len(s)
会调用 s.__len__()
原理是一样的
使用生成器创建新的迭代模式
问题
你想实现一个自定义迭代模式,跟普通的内置函数比如 range()
, reversed()
不一样。
解决方案
如果你想实现一种新的迭代模式,使用一个生成器函数来定义它。 下面是一个生产某个范围内浮点数的生成器
def frange(start, stop, increment):
x = start
while x < stop:
yield x
x += increment
为了使用这个函数, 你可以用for循环迭代它或者使用其他接受一个可迭代对象的函数(比如 sum()
,list()
等)。示例如下:
>>> for n in frange(0, 4, 0.5):
... print(n)
...
0
0.5
1.0
1.5
2.0
2.5
3.0
3.5
>>> list(frange(0, 1, 0.125))
[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875]
>>>
# 讨论
一个函数中需要有一个 yield 语句即可将其转换为一个生成器。 跟普通函数不同的是,生成器只能用于迭代操作。 下面是一个实验,向你展示这样的函数底层工作机制:
>>> def countdown(n):
... print('Starting to count from', n)
... while n > 0:
... yield n
... n -= 1
... print('Done!')
...
>>> # Create the generator, notice no output appears
>>> c = countdown(3)
>>> c
<generator object countdown at 0x1006a0af0>
>>> # Run to first yield and emit a value
>>> next(c)
Starting to count from 3
3
>>> # Run to the next yield
>>> next(c)
2
>>> # Run to next yield
>>> next(c)
1
>>> # Run to next yield (iteration stops)
>>> next(c)
Done!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
一个生成器函数主要特征是它只会回应在迭代中使用到的 next 操作。 一旦生成器函数返回退出,迭代终止。我们在迭代中通常使用的for语句会自动处理这些细节,所以你无需担心。
实现迭代器协议
问题
你想构建一个能支持迭代操作的自定义对象,并希望找到一个能实现迭代协议的简单方法。
解决方案
目前为止,在一个对象上实现迭代最简单的方式是使用一个生成器函数。之前使用Node类来表示树形数据结构。你可能想实现一个以深度优先方式遍历树形节点的生成器。 下面是代码示例:
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
def depth_first(self):
yield self
for c in self:
yield from c.depth_first()
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))
for ch in root.depth_first():
print(ch)
# Outputs Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)
Python的迭代协议要求一个 __iter__()
方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()
方法并通过 StopIteration
异常标识迭代的完成。 但是,实现这些通常会比较繁琐。 下面我们演示下这种方式,如何使用一个关联迭代器类重新实现 depth_first()
方法:
class Node2:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
def depth_first(self):
return DepthFirstIterator(self)
class DepthFirstIterator(object):
'''
Depth-first traversal
'''
def __init__(self, start_node):
self._node = start_node
self._children_iter = None
self._child_iter = None
def __iter__(self):
return self
def __next__(self):
# Return myself if just started; create an iterator for children
if self._children_iter is None:
self._children_iter = iter(self._node)
return self._node
# If processing a child, return its next item
elif self._child_iter:
try:
nextchild = next(self._child_iter)
return nextchild
except StopIteration:
self._child_iter = None
return next(self)
# Advance to the next child and start its iteration
else:
self._child_iter = next(self._children_iter).depth_first()
return next(self)
DepthFirstIterator
类和上面使用生成器的版本工作原理类似, 但是它写起来很繁琐,因为迭代器必须在迭代处理过程中维护大量的状态信息。 坦白来讲,没人愿意写这么晦涩的代码。将你的迭代器定义为一个生成器后一切迎刃而解。
反向迭代
问题
你想反方向迭代一个序列
解决方案
使用内置的 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 iterator
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
# Reverse iterator
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1
for rr in Countdown(30):
print(rr)
定义一个反向迭代器可以使得代码非常的高效, 因为它不再需要将数据填充到一个列表中然后再去反向迭代这个列表。
带有外部状态的生成器函数
问题
你想定义一个生成器函数,但是它会调用某个你想暴露给用户使用的外部状态值。
解决方案
如果你想让你的生成器暴露外部状态给用户, 别忘了你可以简单的将它实现为一个类,然后把生成器函数放到 __iter__()
方法中过去。比如:
from collections import deque
class linehistory:
def __init__(self, lines, histlen=3):
self.lines = lines
self.history = deque(maxlen=histlen)
def __iter__(self):
for lineno, line in enumerate(self.lines, 1):
self.history.append((lineno, line))
yield line
def clear(self):
self.history.clear()
为了使用这个类,你可以将它当做是一个普通的生成器函数。 然而,由于可以创建一个实例对象,于是你可以访问内部属性值, 比如 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='')
讨论
关于生成器,很容易掉进函数无所不能的陷阱。 如果生成器函数需要跟你的程序其他部分打交道的话(比如暴露属性值,允许通过方法调用来控制等等), 可能会导致你的代码异常的复杂。 如果是这种情况的话,可以考虑使用上面介绍的定义类的方式。 在 __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'
>>>