你每天都在写 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

这说明什么。

说明 it1it2 是两个独立的迭代器。
它们各自维护自己的进度。

如果列表本身就是迭代器,那这种重复从头开始遍历就会变麻烦。
所以很多容器对象不会把自己设计成迭代器,而是每次 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 objnext(obj) 不一定都成立
为什么自定义对象只要实现协议就能融入 Python 生态

这些问题,都会越来越清楚。

本章小结

for 循环的本质,不是认识某种具体类型,而是驱动迭代协议:先通过 iter() 拿到迭代器,再不断通过 next() 取下一个值,直到捕获 StopIteration 结束。可迭代对象和迭代器不是一回事,很多容器对象只是能产生迭代器,而迭代器本身负责维护遍历进度。理解这一层,后面学迭代器、生成器和惰性计算会顺很多。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐