for 循环其实就是三个步骤,你完全可以手写出来。看过这三步,第二次遍历变成空的 bug 就不再让人意外,而是理所当然。
上一篇结尾,zip 第二次用的时候变成了空的。本篇讲清原因,这也是整个系列的基础。
for 语句不是原语。它是三件事的简写。
手写一遍循环
names = ['ada', 'grace', 'alan']
it = iter(names)
while True:
try:
name = next(it)
except StopIteration:
break
print(name)
输出:
ada
grace
alan
这就是 for 语句的全部。对这个对象调用 iter()。对得到的结果反复调用 next(),直到抛出 StopIteration。捕获它,然后停下。
除此之外什么都没有。没有按索引遍历列表,也没有检查长度。
可迭代对象、迭代器,以及关键的区别
names = ['ada', 'grace', 'alan']
it = iter(names)
print(type(names).__name__, '->', type(it).__name__)
print('an iterator returns itself: ', iter(it) is it)
print('a list hands out a new one: ', iter(names) is iter(names))
输出:
list -> list_iterator
an iterator returns itself: True
a list hands out a new one: False
可迭代对象是能从中取得迭代器的东西。迭代器是记住你走到哪儿的那个东西。
列表是可迭代对象,本身不保存位置——每次 for 遍历它都从头开始,因为每个 for 都会要一个新的迭代器。你能对同一个列表循环两次,唯一的原因就在这里。
关键全在位置
names = ['ada', 'grace', 'alan']
it = iter(names)
print(next(it))
for name in it: # picks up where next() left off
print('loop:', name)
输出:
ada
loop: grace
loop: alan
for 没有从头开始,因为 iter(it) 返回的还是那个用了一半的迭代器。
下面这件事正是这么来的
scores = zip(['ada', 'grace'], [90, 85])
print('first :', list(scores))
print('second:', list(scores))
输出:
first : [('ada', 90), ('grace', 85)]
second: []
zip 返回的是迭代器,而不是能不断产出新迭代器的可迭代对象。第一次 list() 把它跑到了尽头。第二次发现它已经在尽头——这里的“空”就是这个意思。
map、filter、reversed、enumerate、打开的文件,以及所有生成器,都是这样。如果需要用两次,就先存下来:
pairs = list(zip(['ada', 'grace'], [90, 85]))
print('first :', pairs)
print('second:', pairs)
输出:
first : [('ada', 90), ('grace', 85)]
second: [('ada', 90), ('grace', 85)]
自己写一个
两个方法。__iter__ 返回迭代器,__next__ 返回下一个值,或者抛出 StopIteration。
class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
for i in Countdown(3):
print(i)
c = Countdown(3)
print(list(c))
print(list(c))
输出:
3
2
1
[3, 2, 1]
[]
循环能跑。但最后一行说明它和 zip 有同样的毛病:对象就是自己的迭代器,位置记在它自己身上,所以只能用一遍。
让它可以重复使用
把两份工作分开。容器保持不动,由另一个单独的对象负责遍历。
class CountdownIter:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
class Countdown2:
def __init__(self, n):
self.n = n
def __iter__(self):
return CountdownIter(self.n)
c = Countdown2(3)
print(list(c))
print(list(c))
输出:
[3, 2, 1]
[3, 2, 1]
列表用的就是这种结构,现在 Countdown2 用起来也和列表一样了。
不过,从三倒数到一,也写了二十行。下一篇会把它缩到三行。
next 可以带默认值
想取第一个元素,又不想自己处理为空的情况时,这很好用。
it = iter(['ada'])
print(next(it, 'nobody'))
print(next(it, 'nobody'))
输出:
ada
nobody
没有默认值的话,第二次调用会抛出 StopIteration。
要点
for x in thing的意思是:先iter(thing),再不断next(),直到StopIteration。- 可迭代对象能产出迭代器。迭代器保存位置,并在
__iter__里返回自己。 - 任何返回迭代器的东西——
zip、map、filter、文件、生成器——都只能遍历一遍。 - 把
__iter__和__next__写在同一个对象上,得到的东西只能用一次。想要可重复使用,就让__iter__返回一个新的迭代器。