Blog

Python 迭代器:for 循环到底做了什么

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() 把它跑到了尽头。第二次发现它已经在尽头——这里的“空”就是这个意思。

mapfilterreversedenumerate、打开的文件,以及所有生成器,都是这样。如果需要用两次,就先存下来:

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__ 里返回自己。
  • 任何返回迭代器的东西——zipmapfilter、文件、生成器——都只能遍历一遍。
  • __iter____next__ 写在同一个对象上,得到的东西只能用一次。想要可重复使用,就让 __iter__ 返回一个新的迭代器。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。