Python 迭代器与可迭代对象详解

在 Python 开发中,「遍历」是最常见的操作之一——不管是扫描列表、查看字典键值对,还是逐行读取文件,我们都习惯直接写一个 for ... in ...。但你有没有好奇过:为什么这些东西能放进 for 循环?为什么有些对象可以直接用 next() 取元素? 答案就藏在两个核心概念里:「可迭代对象(Iterable)」和「迭代器(Iterator)」。

可迭代对象 (Iterable)

简单来说,可迭代对象就是“能被 for 循环遍历的对象”。它本身并不直接产生“下一个值”的能力,但可以“变出”一个能逐个产出元素的工具——也就是迭代器。

Python 里常见的可迭代对象可以分为两类:

  • 基础集合类listtupledictsetstrbytes
  • 带延迟特性的对象:生成器(generator)、文件对象、zip()/map()/filter() 的返回值等

怎么判断是不是可迭代的?

使用 isinstance() 配合标准库中的 collections.abc.Iterable

from collections.abc import Iterable

# 基础集合类型
print(isinstance([1, 2], Iterable))        # True
print(isinstance({'a': 1}, Iterable))      # True
print(isinstance('hello', Iterable))       # True

# 延迟对象
print(isinstance((x for x in range(3)), Iterable))  # True
print(isinstance(open('test.txt', 'r'), Iterable))  # True(别忘了关闭文件!)

# 非可迭代类型
print(isinstance(100, Iterable))           # False
print(isinstance(True, Iterable))          # False

迭代器 (Iterator)

迭代器是可迭代对象的“进阶版”——它不仅可以进入 for 循环,还能被 next() 主动调用,逐个取出元素,直到耗尽并抛出 StopIteration 异常。可以把它理解为“手握数据流控制权的工具”。

怎么判断是不是迭代器?

同样使用 isinstance(),这次搭配 collections.abc.Iterator

from collections.abc import Iterator

# 生成器是迭代器!
print(isinstance((x for x in range(3)), Iterator))  # True

# 通过 iter() 转换后得到的也是迭代器
print(isinstance(iter([]), Iterator))               # True

# 基础集合本身不是迭代器!
print(isinstance([1, 2], Iterator))                 # False
print(isinstance('hello', Iterator))                # False

从可迭代对象得到迭代器

使用内置函数 iter() 即可轻松转换:

nums = [1, 2, 3]
it_nums = iter(nums)  # 拿到迭代器

print(next(it_nums))  # 1
print(next(it_nums))  # 2
print(next(it_nums))  # 3
print(next(it_nums))  # 抛出 StopIteration 异常!

注意:迭代器非常“懒”,只有当你调用 next() 时它才会计算并返回下一个值,这为处理大数据量或无限序列提供了可能。

迭代器 vs 可迭代对象:到底差在哪?

有时单看文字容易绕,我们用一张对比表和实际场景把区别讲透:

核心特性可迭代对象(Iterable)迭代器(Iterator)
能否直接放进 for 循环
能否用 next() 主动取值
是否支持惰性计算大部分不支持(特殊对象除外)✅ 绝对支持
能否表示无限序列❌(内存根本装不下)
内存占用固定(需预存所有元素)极低(只维护当前计算逻辑)

举个最直观的无限自然数序列的例子:

  • 用列表实现?不可能——[0, 1, 2, …] 早晚把内存写爆。
  • 用迭代器实现?轻轻松松(后面马上会自定义一个给你看)。

再看一个内存占用对比的例子(在 CPython 下,整数的内存开销因小整数池等因素而有所不同,但这里主要看趋势):

# 生成 100 万个平方数的列表 —— 需要提前分配大量内存
big_list = [x*x for x in range(1_000_000)]

# 生成 100 万个平方数的迭代器 —— 只占用几十字节!
big_iterator = (x*x for x in range(1_000_000))

你甚至可以在只创建迭代器后立即用 next() 按需取值,完全不用等全部生成完。

for 循环的“真面目”

你写的每一行 for x in xxx,Python 底层都会自动完成这三件事:

  1. 调用 iter(xxx) 获取对应的迭代器对象
  2. 循环调用 next(迭代器),并将结果赋值给 x
  3. 一旦捕获到 StopIteration 异常,就优雅地退出循环

比如这段简单的代码:

for num in [1, 2, 3]:
    print(num)

Python 内部其实等价于执行以下逻辑:

# 第一步:获取迭代器
it = iter([1, 2, 3])

# 第二步 + 第三步:循环取值 + 异常退出
while True:
    try:
        num = next(it)
        print(num)
    except StopIteration:
        break

现在是不是觉得 for 循环没那么“神秘”了?

实用开发建议

1. 处理大数据/无限序列,优先考虑迭代器或生成器

当你要处理几十万、上百万行的文件,或者要遍历无限序列时,请绕开列表推导式,改用生成器表达式或自定义迭代器,否则内存很快就会被撑爆。

# ❌ 一次性把整个文件读进内存(危险)
lines = [line.strip() for line in open('huge_file.log')]

# ✅ 用生成器表达式按行处理(安全)
lines_safe = (line.strip() for line in open('huge_file.log'))
for line in lines_safe:
    # 逐行处理
    pass

2. 自定义迭代器,只需要两个方法

如果你想自己写一个迭代器类(比如实现刚才提到的无限自然数),必须实现两个特殊方法:

  • __iter__()必须返回迭代器自身(self,这是迭代器与可迭代对象在协议上保持统一的关键
  • __next__()负责“取出当前元素、移动到下一个元素、或在结束时抛出 StopIteration

直接上代码,实现一个从指定起点开始的无限自然数迭代器:

from collections.abc import Iterator

class InfiniteNaturals(Iterator):
    def __init__(self, start=0):
        self.current = start
    
    # 返回自身
    def __iter__(self):
        return self
    
    # 真正“取下一个值”的逻辑
    def __next__(self):
        # 这里没有终止条件——真的是无限序列!
        result = self.current
        self.current += 1
        return result

# 测试
nat = InfiniteNaturals(10)
print(next(nat))  # 10
print(next(nat))  # 11
# 注意:如果直接用 for 循环 nat,会进入无限循环哦!

如果你的序列有尽头,只需在 __next__() 中加上终止条件即可:

class RangeCustom(Iterator):
    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     # 到达终点,主动终止
        result = self.current
        self.current += 1
        return result

# 测试
for num in RangeCustom(2, 5):
    print(num)   # 依次输出 2, 3, 4

3. 迭代器是“一次性”的!

这个特性非常容易踩坑:迭代器遍历完一次就空了,再用 next()for 循环都无法获得任何值。

it = iter([1, 2, 3])
print(list(it))  # [1, 2, 3]
print(list(it))  # [] —— 空的!

小贴士:如果你需要重复遍历,应该每次重新获取新的迭代器,或者直接保存原始可迭代对象(如列表),需要时再调用 iter()

总结

彻底理解迭代器和可迭代对象,是写出高效、Pythonic、内存友好代码的重要基石。让我们用一句话再巩固一下:

  • 可迭代对象是“原料/容器”,它能“提供”迭代器
  • 迭代器是“工具/工人”,它握着惰性计算的逻辑,按需产出下一个值
  • for 循环只是迭代器模式的语法糖
  • 自定义迭代器需要实现 __iter__()(返回自身)和 __next__()

下次写遍历代码时,不妨多想一步:这里用列表好,还是用生成器或迭代器更合适?你的程序可能会因此变得更加优雅和健壮。