《Python 高阶教程》017|for 循环背后发生了什么:迭代协议完整拆解
你每天都在写 for,但大多数人并不知道它到底在做什么
Python 里,for 是最常见的语法之一。
遍历列表用它。
遍历字符串用它。
遍历字典、集合、文件对象也用它。
甚至很多自定义对象,只要设计得对,也能被 for 直接遍历。
也正因为太常见,很多人会自然觉得:
for 不就是一个循环吗,拿元素一个个跑一遍而已。
这种理解不能说错,但还不够深。
真正进入高阶 Python 后,你会发现,for 之所以这么灵活,不是因为它认识列表、字符串、字典这些具体类型,而是因为它遵守了一套统一规则。只要对象满足这套规则,for 就能工作。
这套规则,就是迭代协议。
这一章要讲透的,不只是“for 怎么用”,而是“for 背后为什么能这么用”。看懂这一层,后面学迭代器、生成器、推导式、yield、异步迭代时,思路会顺很多。
先看一个最普通的 for,别急着把它想简单了
nums = [10, 20, 30]
for num in nums:
print(num)
这段代码当然很熟。
输出就是 10、20、30。
但问题来了。
为什么 for 知道先拿 10,再拿 20,再拿 30。
为什么它知道什么时候该停。
为什么同样的 for,换成字符串也能跑:
text = "python"
for ch in text:
print(ch)
再换成字典键也能跑:
data = {"name": "Tom", "age": 20}
for key in data:
print(key)
如果 for 真的是给每种数据类型分别写了专属支持,那这套设计会非常笨重。
Python 并不是这么干的。
它真正依赖的是一种更通用的能力:
对象能不能一个一个地产出元素,并在没有元素时明确告诉我该停了。
这就是迭代协议要解决的事。
先记住一句最重要的话:for 不关心你是谁,只关心你能不能迭代
for 并不需要知道某个对象到底是列表、元组、字符串还是别的什么。
它关心的是:
这个对象能不能提供一个迭代器
这个迭代器能不能不断给出下一个元素
没有元素时,能不能抛出停止信号
只要能做到这三件事,for 就能遍历它。
所以 for 的真正本质,不是“遍历列表”,而是“驱动一个迭代器不断取下一个值”。
这个角度一换,很多原本零散的现象就会突然统一起来。
什么是可迭代对象,先从最直观的感觉建立起来
平时能直接放进 for 的对象,通常都叫可迭代对象。
比如:
列表
元组
字符串
字典
集合
range 对象
文件对象
看几个例子:
for x in [1, 2, 3]:
print(x)
for x in (1, 2, 3):
print(x)
for x in "abc":
print(x)
for x in range(3):
print(x)
这些对象有一个共同点:
你可以从里面按顺序一个一个拿出元素。
所以可迭代对象,说白了就是:
可以被 for 遍历的对象。
但这里先别急着把“可迭代对象”和“迭代器”混为一谈。
它们关系很近,但不是一回事。后面马上就会讲清。
可迭代对象和迭代器,不是同一个东西
这是很多人最容易混淆的地方。
先看代码:
nums = [1, 2, 3]
print(iter(nums))
这里的 iter(nums) 会返回一个新对象。
这个新对象,才是真正供 for 使用的迭代器。
也就是说:
列表是可迭代对象
由 iter(列表) 得到的结果,是迭代器
这就像什么。
一本书是可阅读对象。
你真正一页页翻的时候,手里拿着的是“当前阅读进度”。
列表本身只是“可以被迭代”。
真正负责“下一步拿谁”的,是它对应的迭代器。
这个区别特别关键。
因为后面你会发现:
不是所有可迭代对象本身都是迭代器
但迭代器一定是可迭代的
这句话先记住,后面会验证。
那迭代器到底是什么
迭代器本质上是一个“记得住当前进度,并且能继续往后取值”的对象。
它至少要支持两件事:
第一,能返回自己作为可迭代对象
第二,能不断给出下一个元素
在 Python 里,这对应两个方法:
__iter__()
__next__()
你可以先把它们理解成:
__iter__():告诉别人,我能提供迭代器
__next__():给我下一个值
如果没有下一个值了,__next__() 不会返回 None,也不会随便报错,而是要抛出一个非常关键的异常:
StopIteration
这个异常,就是“遍历结束”的标准信号。
先手工体验一下迭代器怎么工作
看代码:
nums = [10, 20, 30]
it = iter(nums)
print(next(it))
print(next(it))
print(next(it))
输出是:
10
20
30
如果你再来一次:
print(next(it))
就会报错:
StopIteration
这说明什么。
说明迭代器并不是每次都从头来。
它会记住当前位置。
每调用一次 next(it),它就向前走一步。
走到头时,就抛出 StopIteration。
这就是 for 循环背后最核心的机械动作。
for 循环,手工展开后大概是什么样子
前面这段代码:
nums = [10, 20, 30]
for num in nums:
print(num)
大致可以理解成下面这种形式:
nums = [10, 20, 30]
it = iter(nums)
while True:
try:
num = next(it)
print(num)
except StopIteration:
break
这段代码特别重要。
因为它几乎把 for 的底层思路暴露出来了。
先把可迭代对象变成迭代器。
然后不断调用 next 取下一个值。
取不到了,就捕获 StopIteration 退出循环。
你以后看到任何 for,本质上都可以先在脑子里还原成这个模型。
为什么字符串、列表、字典都能统一工作
因为它们都能提供迭代器。
比如字符串:
text = "abc"
it = iter(text)
print(next(it))
print(next(it))
print(next(it))
输出是:
a
b
c
列表也一样:
nums = [1, 2, 3]
it = iter(nums)
print(next(it))
print(next(it))
print(next(it))
字典默认迭代的是键:
data = {"name": "Tom", "age": 20}
it = iter(data)
print(next(it))
print(next(it))
所以 Python 的统一设计特别漂亮。
for 不需要关心底层类型细节。
对象自己只要能交出一个合格的迭代器,for 就能驱动它工作。
这就是协议式设计的威力:
不是靠写死类型判断,而是靠共同遵守一套接口规则。
iter 和 next,其实是理解 for 的两把钥匙
很多人平时几乎不用 iter() 和 next(),但理解迭代协议时,它们特别重要。
iter(obj):向对象要一个迭代器
next(it):向迭代器要下一个值
只要你把这两步看透,for 就不再神秘。
例如:
colors = ["red", "green", "blue"]
it = iter(colors)
print(next(it))
print(next(it))
print(next(it))
你会很直接地感受到:
不是 for 在魔法遍历
而是迭代器在一点点吐出值
for 只是帮你把这些细节包装得更自然一些。
不是所有对象都能直接 next
这一点也很重要。
看代码:
nums = [1, 2, 3]
print(next(nums))
这会报错。
因为列表本身不是迭代器。
你得先这样:
it = iter(nums)
print(next(it))
所以要分清两层:
列表可迭代
但列表不是迭代器
这个区别很容易被忽略,因为平时 for 帮你自动做了第一步。
可一旦自己手工操作,就必须先 iter() 再 next()。
那为什么有些对象既是可迭代对象,又是迭代器
比如文件对象,很多时候就有这种特征。
再比如你后面会学到的生成器,也是既可迭代又是迭代器。
这类对象通常满足:
调用 iter(obj) 时,返回的就是它自己
同时它自己也支持 next()
所以前面那句话就能更好理解了:
可迭代对象不一定是迭代器
但迭代器一定是可迭代的
迭代器通常会实现:
def __iter__(self):
return self
这样它自己就是自己的迭代器。
手写一个最简单的迭代器,你会一下子明白很多
来看一个完全手工实现的例子。
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
counter = CountDown(3)
print(next(counter))
print(next(counter))
print(next(counter))
输出是:
3
2
1
如果再 next 一次,就会抛出 StopIteration。
这个例子特别重要。
因为它把迭代器最核心的三件事全摆出来了:
保存状态,这里是 self.current
逐步产出值,这里是 return value
结束时抛出 StopIteration
一旦自己手写过这种东西,再看 for 背后的机制就会清晰很多。
这个迭代器也能直接放进 for
因为它本身已经是可迭代对象加迭代器:
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for x in CountDown(5):
print(x)
输出就是 5 到 1。
这里你会更明显地体会到:
for 并不关心这个对象是什么类型。
它只关心你是不是满足迭代协议。
所以自定义对象想支持 for,核心不是“继承某个特殊父类”,而是“把协议做好”。
可迭代对象也可以不自己实现 next,而是返回另一个迭代器
前面的 CountDown 是自己既当可迭代对象又当迭代器。
但很多对象更常见的做法是:
自己只是可迭代对象
__iter__() 返回一个专门的迭代器
看个简单例子:
class MyRange:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return MyRangeIterator(self.start, self.end)
class MyRangeIterator:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
for x in MyRange(2, 5):
print(x)
输出是:
2
3
4
这里的职责划分很清楚:
MyRange 负责表示一个可迭代范围
MyRangeIterator 负责记住当前位置并不断给出下一个值
这种设计在复杂场景里会更合理。
因为可迭代对象和迭代进度分开了。
为什么把可迭代对象和迭代器分开,有时很重要
因为这样同一个可迭代对象可以反复产生全新的迭代器。
比如列表:
nums = [1, 2, 3]
it1 = iter(nums)
it2 = iter(nums)
print(next(it1))
print(next(it1))
print(next(it2))
输出通常是:
1
2
1
这说明什么。
说明 it1 和 it2 是两个独立的迭代器。
它们各自维护自己的进度。
如果列表本身就是迭代器,那这种重复从头开始遍历就会变麻烦。
所以很多容器对象不会把自己设计成迭代器,而是每次 iter() 都返回一个新的迭代器。
这个区别在实际开发里特别重要。
为什么有些对象遍历一次后就“空了”
比如生成器和某些文件对象,常常会让人有这种感觉:
第一次遍历有内容
第二次遍历就没了
这通常是因为它们本身就是迭代器,进度已经走到了末尾。
不像列表那样,每次 iter() 都能轻松得到一个全新的遍历起点。
先看一个简化感受:
it = iter([1, 2, 3])
for x in it:
print(x)
for x in it:
print(x)
第二个 for 就不会再输出了。
因为迭代器已经耗尽了。
这就是“可迭代对象”和“迭代器”差异带来的实际现象。
所以以后只要碰到“为什么第二次遍历没东西了”,就要先想:
我手里拿着的,是容器,还是已经消耗过的迭代器。
for 不是把所有元素一次性拿出来,而是懒惰地一个个取
这一点特别重要。
很多人潜意识里觉得:
for 一开始是不是就把所有值准备好了。
其实不是。
它通常是每次循环时,才向迭代器要下一个值。
这意味着什么。
意味着:
节省内存
可以处理很长的数据流
可以处理甚至理论上无限长的序列
可以按需计算,而不是一次性全算完
比如 range:
for x in range(1000000000):
if x == 3:
print(x)
break
这段代码不会提前把十亿个数字全装进列表里。
它是边要边给。
所以迭代协议背后,其实还隐藏着一个非常强大的思想:
按需提供数据。
后面学生成器时,这一点会体现得更明显。
为什么字典 for 默认拿到的是键
看例子:
data = {"name": "Tom", "age": 20}
for item in data:
print(item)
输出是:
name
age
因为字典对象定义自己的迭代行为时,默认返回的是键。
如果你想遍历值,可以写:
for value in data.values():
print(value)
如果想遍历键值对:
for key, value in data.items():
print(key, value)
这进一步说明,for 不决定“遍历出什么”。
它只负责驱动迭代器。
真正决定产出内容的,是对象自己的迭代实现。
所以列表迭代出元素,字符串迭代出字符,字典默认迭代出键,本质上都来自各自的协议实现。
文件为什么也能 for,一点都不特殊
比如:
with open("data.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
这里 for 能逐行读取文件,不是因为文件语法特殊,而是因为文件对象本身支持迭代协议。
它的迭代器每次给出一行,直到文件读完,再抛出 StopIteration。
你会发现,这种统一设计特别舒服。
不管是列表、字符串、文件,for 的底层驱动逻辑几乎都是同一套。
差别只在于:下一个值到底是什么。
判断一个对象能不能被 for,最直接的方法是什么
最简单的判断方式,就是试试 iter():
obj = [1, 2, 3]
it = iter(obj)
print(it)
如果 iter(obj) 能成功,通常说明它是可迭代对象。
如果报错,比如:
obj = 123
iter(obj)
就会出类似这样的错误:
TypeError: 'int' object is not iterable
说明整数不是可迭代对象。
所以你以后看到某个对象能不能 for,脑子里可以先快速翻译成一句话:
它能不能交出一个迭代器。
for 和 while 的本质差别,不只是写法
很多人会说,for 不就是更省事的 while 吗。
这话只说对了一半。
从底层迭代驱动来说,for 确实可以还原成 while + iter + next + StopIteration。
但语义上,for 更高级。
因为:
for 表达的是“遍历一个可迭代对象”
while 表达的是“只要条件成立就继续执行”
看两个例子。
for:
for x in [1, 2, 3]:
print(x)
while:
nums = [1, 2, 3]
i = 0
while i < len(nums):
print(nums[i])
i += 1
第二种当然也能完成任务。
但它依赖索引,依赖长度,还得自己维护位置。
如果对象根本没有索引呢,比如文件对象、生成器、自定义迭代器,这套办法就不自然了。
所以 for 真正厉害的地方,是它抽象掉了“怎么拿下一个值”的过程。
你只用关心“我要一个个处理这些值”。
为什么很多 Python 风格建议优先用 for
因为 for 更贴近迭代协议,也更贴近问题本身。
如果你的目标是遍历元素,那直接写 for,通常最自然。
不需要自己维护计数器、索引、边界条件。
比如:
for line in f:
...
明显比:
while True:
line = f.readline()
if not line:
break
...
更清晰。
这不是说 while 不行,而是 for 已经把“按迭代协议一个个拿值直到结束”这件事抽象得非常成熟了。
能直接表达意图时,就没必要退回更底层的控制方式。
手写一个支持 for 的对象,你会真正理解协议的价值
比如实现一个班级点名器,每次遍历返回一个学生名:
class StudentIterator:
def __init__(self, students):
self.students = students
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.students):
raise StopIteration
student = self.students[self.index]
self.index += 1
return student
class ClassRoom:
def __init__(self, students):
self.students = students
def __iter__(self):
return StudentIterator(self.students)
room = ClassRoom(["张三", "李四", "王五"])
for name in room:
print(name)
这里你能非常直接地看到:
ClassRoom 只是可迭代对象
真正逐个给名字的是 StudentIterator
这类例子特别能帮助你把抽象概念落地。
为什么说迭代协议是一种非常 Python 的设计方式
因为它体现了 Python 很典型的一种风格:
不强依赖具体类型
而是看你是否满足某个协议
只要你能:
提供 __iter__()
返回一个能 __next__() 的对象
在结束时抛出 StopIteration
你就能融入整套 for、推导式、很多内置函数的生态。
这和“必须继承某个类才能使用”那种思路不太一样。
它更灵活,也更符合 Python 的鸭子类型精神。
你不像列表没关系。
你只要会像列表那样被迭代,就够了。
很多内置函数其实也依赖迭代协议
比如:
list()
tuple()
set()
sum()
max()
min()
any()
all()
这些函数很多都能接受可迭代对象。
例如:
nums = [1, 2, 3]
print(sum(nums))
print(max(nums))
print(list(iter(nums)))
如果你自定义对象把迭代协议做好了,很多内置函数也会直接受益。
比如前面的 ClassRoom:
room = ClassRoom(["张三", "李四", "王五"])
print(list(room))
这就能直接转成列表。
所以迭代协议不是只服务于 for。
它是 Python 一整套数据处理生态的公共接口之一。
从这一章开始,要建立一个新的阅读习惯
以后你看到一个能 for 的对象,不要只觉得“它能遍历”。
你应该多想一步:
它的迭代器是谁
next 从哪拿值
什么时候停
它是每次都能重新开始,还是一次性消耗
它产出的到底是元素、键、行,还是别的什么
这个习惯一旦养成,很多高阶特性就会顺很多。
比如:
为什么生成器只能走一遍
为什么有些对象第二次遍历没值了
为什么 for x in obj 和 next(obj) 不一定都成立
为什么自定义对象只要实现协议就能融入 Python 生态
这些问题,都会越来越清楚。
本章小结
for 循环的本质,不是认识某种具体类型,而是驱动迭代协议:先通过 iter() 拿到迭代器,再不断通过 next() 取下一个值,直到捕获 StopIteration 结束。可迭代对象和迭代器不是一回事,很多容器对象只是能产生迭代器,而迭代器本身负责维护遍历进度。理解这一层,后面学迭代器、生成器和惰性计算会顺很多。
更多推荐



所有评论(0)