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)) print (next (it)) print (next (it)) print (next (it))
二、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)
问题 :这个 Counter 只能迭代一次——self.i 变了就回不去。
1 2 3 c = Counter(1 , 5 ) list (c) 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) list (r)
内置的 list、str、dict 都是这种”可迭代对象 + 独立迭代器”的设计。
四、更简单:用 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)
比手写 __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 sum ([x*x for x in range (10 **9 )]) sum (x*x for x in range (10 **9 ))
range、map、filter、zip、enumerate、open 全都是惰性的。
八、迭代器只能走一次 1 2 3 it = iter ([1 , 2 , 3 ]) list (it) list (it)
想反复遍历,要么保留原始可迭代对象,要么每次 iter(source) 造新迭代器。
标准库 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, repeatfor i in islice(count(10 ), 5 ): print (i) for x in islice(cycle([1 , 2 , 3 ]), 8 ): print (x) list (chain([1 , 2 ], [3 , 4 ], [5 ])) list (islice(range (100 ), 10 , 20 ))
十、检查可迭代性 1 2 3 4 5 from collections.abc import Iterable, Iteratorisinstance ([1 , 2 ], Iterable) isinstance ([1 , 2 ], Iterator) isinstance (iter ([1 , 2 ]), Iterator)
十一、enumerate / zip 用法回顾 1 2 3 4 5 6 7 8 9 10 11 for i, x in enumerate (items, start=1 ): print (f"{i} . {x} " ) for a, b in zip (names, ages): print (a, b) for a, b in zip (names, ages, strict=True ): ...
十二、迭代器的组合 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 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 """把一个可迭代对象按 n 个一组切分。""" from itertools import islicefrom collections.abc import Iterator, Iterabledef 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) 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) print (paginate(range (20 ), page_size=5 , page=3 )) p = PeekableIterator([1 , 2 , 3 ]) print (p.peek()) print (p.peek()) print (next (p)) print (next (p))
十四、可迭代对象拆包 用星号解包:
1 2 first, *rest = range (5 ) *init, last = "abcde"
这里的 rest 类型是 list,即使右侧是任意可迭代对象。
十五、常见陷阱
迭代器只能走一次 :想反复用要保存原始序列。
在遍历中修改可迭代对象 :list 中断、set/dict 抛 RuntimeError。
忘 raise StopIteration :手写迭代器无限循环。
__iter__ 直接 return self(但状态不重置) :多次遍历只成功一次。
for x in it: ...;for x in it: ... :第二个循环什么都不出,因为 it 已耗尽。
把生成器传给 len() :TypeError,先 list() 转。
十六、小结与延伸阅读
可迭代对象有 __iter__,迭代器有 __next__;
for 循环调 iter() + 循环 next() + 捕 StopIteration;
自定义迭代器优先用生成器函数;
迭代器是惰性的,处理大数据首选;
iter(callable, sentinel) 有奇效;
迭代器只能走一次;
itertools 是迭代器工具库。
延伸阅读:
下一篇 生成器 yield 深入 我们讲写迭代器的最优雅方式——yield。