迭代器与可迭代对象

for x in obj: 背后到底发生了什么?为什么列表、字典、字符串、文件、range 都能这么用?答案就是 Python 的迭代协议——__iter____next__。理解迭代器,你就能写出能被 for 循环、内置函数(sum/max/any 等)无缝使用的自定义类,也能明白生成器(下一篇)为什么那么强大。

一、可迭代对象 vs 迭代器

两个概念要分清:

  • 可迭代对象(Iterable):能被 for 用的东西,有 __iter__ 方法;
  • 迭代器(Iterator):真正”产生下一个值”的对象,有 __next__ 方法(同时也有 __iter__)。

关系:每个迭代器都是可迭代对象,反过来不成立

举例:

  • 列表 [1, 2, 3] 是可迭代对象,但不是迭代器;
  • iter([1, 2, 3]) 返回一个迭代器。
1
2
3
4
5
6
7
lst = [1, 2, 3]
it = iter(lst) # 拿到迭代器

print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it)) # StopIteration

二、for 循环的底层

看这段代码:

1
2
for x in [10, 20, 30]:
print(x)

Python 实际做的是:

1
2
3
4
5
6
7
_iter = iter([10, 20, 30])     # 拿迭代器
while True:
try:
x = next(_iter) # 取下一个
except StopIteration:
break
print(x)

任何实现了 __iter__ 的对象都能被 for 循环。这就是”鸭子协议”在 Python 里的强大之处。

三、自定义可迭代对象

方式 1:让类同时是迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Counter:
def __init__(self, start, stop):
self.i = start
self.stop = stop

def __iter__(self):
return self # 自己就是迭代器

def __next__(self):
if self.i >= self.stop:
raise StopIteration
v = self.i
self.i += 1
return v


for x in Counter(1, 5):
print(x)
# 1 2 3 4

问题:这个 Counter 只能迭代一次——self.i 变了就回不去。

1
2
3
c = Counter(1, 5)
list(c) # [1, 2, 3, 4]
list(c) # [] 已经走完了!

方式 2:可迭代对象 + 独立迭代器(推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Range:
def __init__(self, start, stop):
self.start, self.stop = start, stop

def __iter__(self):
return RangeIter(self.start, self.stop) # 每次返回新迭代器


class RangeIter:
def __init__(self, start, stop):
self.i, self.stop = start, stop

def __iter__(self):
return self

def __next__(self):
if self.i >= self.stop:
raise StopIteration
v = self.i
self.i += 1
return v


r = Range(1, 5)
list(r) # [1, 2, 3, 4]
list(r) # [1, 2, 3, 4] 可以反复遍历

内置的 liststrdict 都是这种”可迭代对象 + 独立迭代器”的设计。

四、更简单:用 yield(预告)

写迭代器最优雅的方式是生成器函数

1
2
3
4
5
6
7
8
9
def counter(start, stop):
while start < stop:
yield start
start += 1


for x in counter(1, 5):
print(x)
# 1 2 3 4

比手写 __iter__ / __next__ 简单太多。生成器我们下一篇 生成器 yield 深入 详细讲。

五、常见的可迭代对象

  • 序列:list, tuple, str, bytes, range;
  • 集合类:set, frozenset;
  • 映射:dict(默认迭代 key),dict.values(), dict.items();
  • 文件对象:每次迭代产生一行;
  • 迭代器/生成器:自身就可迭代;
  • map/filter/zip/enumerate:都返回迭代器;
  • NumPy array、pandas DataFrame:可迭代。

六、iter 的另一种用法

iter(callable, sentinel) 反复调用 callable,直到返回 sentinel:

1
2
3
4
# 读文件直到遇到空行
with open("data.txt") as f:
for line in iter(f.readline, ""):
print(line.rstrip())

场景:读 socket、循环读输入等。

七、”惰性求值”的威力

迭代器最大的价值是惰性——一次只产生一个元素:

1
2
3
4
5
# 内存占 8 * 10^9 = 8GB
sum([x*x for x in range(10**9)]) # ❌ OOM

# 内存 O(1)
sum(x*x for x in range(10**9)) # ✅ 生成表达式,慢但能跑完

rangemapfilterzipenumerateopen 全都是惰性的。

八、迭代器只能走一次

1
2
3
it = iter([1, 2, 3])
list(it) # [1, 2, 3]
list(it) # [] 空了

想反复遍历,要么保留原始可迭代对象,要么每次 iter(source) 造新迭代器。

九、itertools:迭代器工具箱

标准库 itertools 提供了一堆迭代器工具(详细见 itertools 与 functools):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from itertools import count, cycle, chain, islice, repeat

# 无限数字
for i in islice(count(10), 5):
print(i)
# 10 11 12 13 14

# 无限循环
for x in islice(cycle([1, 2, 3]), 8):
print(x)
# 1 2 3 1 2 3 1 2

# 拼接多个序列
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]

# 截取
list(islice(range(100), 10, 20)) # [10..19]

十、检查可迭代性

1
2
3
4
5
from collections.abc import Iterable, Iterator

isinstance([1, 2], Iterable) # True
isinstance([1, 2], Iterator) # False
isinstance(iter([1, 2]), Iterator) # True

十一、enumerate / zip 用法回顾

1
2
3
4
5
6
7
8
9
10
11
# enumerate 加索引
for i, x in enumerate(items, start=1):
print(f"{i}. {x}")

# zip 并行迭代
for a, b in zip(names, ages):
print(a, b)

# zip 长度不等:以最短为准,严格模式
for a, b in zip(names, ages, strict=True): # 3.10+
...

十二、迭代器的组合

1
2
3
4
5
6
7
8
9
# 找第一个满足条件的
def first(pred, iterable, default=None):
for x in iterable:
if pred(x):
return x
return default

# 或者用 next + 生成表达式
first_neg = next((x for x in nums if x < 0), None)

十三、一个完整例子:分块迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# chunker.py
"""把一个可迭代对象按 n 个一组切分。"""

from itertools import islice
from collections.abc import Iterator, Iterable


def chunked(iterable: Iterable, size: int) -> Iterator[list]:
"""惰性地把 iterable 按 size 分组。"""
it = iter(iterable)
while True:
chunk = list(islice(it, size))
if not chunk:
break
yield chunk


def paginate(items: Iterable, page_size: int, page: int) -> list:
"""获取第 page 页(从 1 开始)。"""
it = iter(items)
# 跳过前 (page-1) * page_size 个
for _ in range((page - 1) * page_size):
next(it, None)
return list(islice(it, page_size))


class PeekableIterator:
"""支持 peek 的迭代器包装。"""

_SENTINEL = object()

def __init__(self, source):
self._it = iter(source)
self._cache = self._SENTINEL

def __iter__(self):
return self

def peek(self, default=None):
if self._cache is self._SENTINEL:
try:
self._cache = next(self._it)
except StopIteration:
return default
return self._cache

def __next__(self):
if self._cache is not self._SENTINEL:
v = self._cache
self._cache = self._SENTINEL
return v
return next(self._it)


if __name__ == "__main__":
# 分块
for chunk in chunked(range(11), 3):
print(chunk)
# [0, 1, 2] [3, 4, 5] [6, 7, 8] [9, 10]

# 分页
print(paginate(range(20), page_size=5, page=3)) # [10, 11, 12, 13, 14]

# peek
p = PeekableIterator([1, 2, 3])
print(p.peek()) # 1
print(p.peek()) # 1(没消耗)
print(next(p)) # 1
print(next(p)) # 2

十四、可迭代对象拆包

用星号解包:

1
2
first, *rest = range(5)         # first=0, rest=[1, 2, 3, 4]
*init, last = "abcde" # init=['a', 'b', 'c', 'd'], last='e'

这里的 rest 类型是 list,即使右侧是任意可迭代对象。

十五、常见陷阱

  1. 迭代器只能走一次:想反复用要保存原始序列。
  2. 在遍历中修改可迭代对象:list 中断、set/dict 抛 RuntimeError。
  3. raise StopIteration:手写迭代器无限循环。
  4. __iter__ 直接 return self(但状态不重置):多次遍历只成功一次。
  5. for x in it: ...for x in it: ...:第二个循环什么都不出,因为 it 已耗尽。
  6. 把生成器传给 len():TypeError,先 list() 转。

十六、小结与延伸阅读

  • 可迭代对象有 __iter__,迭代器有 __next__
  • for 循环调 iter() + 循环 next() + 捕 StopIteration;
  • 自定义迭代器优先用生成器函数;
  • 迭代器是惰性的,处理大数据首选;
  • iter(callable, sentinel) 有奇效;
  • 迭代器只能走一次;
  • itertools 是迭代器工具库。

延伸阅读:

下一篇 生成器 yield 深入 我们讲写迭代器的最优雅方式——yield。