可迭代对象、迭代器与生成器 —— 迭代协议的系统性解析

在基于昨日的基础上,我们今天继续学习可迭代对象 & 迭代器 & 生成器这部分内容;Python 的 for 循环机制虽直观易用,但其底层运作原理常被忽略。为何列表、字符串等类型支持循环,而数值类型不能?为何某些对象只能被遍历一次?这些现象均指向 迭代协议(Iteration Protocol)这一核心概念。本文旨在系统梳理可迭代对象(Iterable)、迭代器(Iterator)与生成器(Generator)之间的层次关系与实现细节。


一、基本概念界定

  • 可迭代对象(Iterable):指能够被 for 语句遍历的对象,如列表、字符串、字典、集合及文件对象等。
  • 迭代器(Iterator):在可迭代对象的基础上,额外具备状态记录能力,能够记住当前遍历位置,并逐一返回后续元素。迭代器自身亦是可迭代对象。
  • 生成器(Generator):一种特殊的迭代器,通过 yield 关键字定义,其书写形式类似函数,而执行行为符合迭代器协议。

层次关系
生成器迭代器可迭代对象
即:所有生成器均为迭代器,所有迭代器均为可迭代对象,但逆命题不成立。


二、类型判定方法

借助 collections.abc 模块(或旧版 collections)中的 IterableIteratorGenerator 抽象基类,可通过 isinstance() 函数进行精确类型检测。

from collections.abc import Iterable, Iterator, Generator

# 常见内置类型检测示例
num = 123
s = "abc"
lst = [1,2,3]
tup = (1,2,3)
d = {"a":1, "b":2}
st = {1,2,3}
r = range(5)

print(isinstance(num, Iterable))  # False(数值类型不支持)
print(isinstance(s, Iterable))    # True
print(isinstance(lst, Iterable))  # True
print(isinstance(r, Iterable))    # True(range 对象为可迭代对象)

# 上述类型均非迭代器
print(isinstance(lst, Iterator))  # False
print(isinstance(r, Iterator))    # False

特殊内置对象reversed()zip()enumerate() 等函数的返回值均为迭代器。

rev = reversed([1,2,3])
zip_obj = zip([1,2], ['a','b'])
enum_obj = enumerate([10,20])

print(isinstance(rev, Iterator))   # True
print(isinstance(zip_obj, Iterator)) # True
print(isinstance(enum_obj, Iterator)) # True

注意:isinstance(obj, Iterable) 主要检测对象是否实现 __iter__() 方法。但某些对象仅通过 __getitem__() 方法亦可被 for 遍历(例如自定义序列类),此时 isinstance 可能返回 False,而 for 循环仍能正常工作。后文将就此展开讨论。


三、可迭代对象 —— 被 for 遍历的充要条件

一个对象若满足以下任一条件,即为可迭代对象:

  1. 实现迭代协议:拥有 __iter__() 方法,且该方法返回一个迭代器。
  2. 实现序列协议:拥有 __getitem__() 方法,且接受从 0 开始的整数索引并顺序返回元素(适用于旧式序列)。

Python 内置的字符串、列表、元组、字典、集合及 range 对象均实现 __iter__(),因此均为可迭代对象。而数值类型未实现上述任一协议,故不可迭代。

自定义可迭代对象的两种实现方式

方式一:实现 __iter__()(标准推荐)

class MyIterable:
    def __init__(self, data):
        self.data = data
    def __iter__(self):
        # 返回一个迭代器实例(通常为自身或专门的迭代器类对象)
        return MyIterator(self.data)

方式二:仅实现 __getitem__()(备选方案)

class MySequence:
    def __init__(self, data):
        self.data = data
    def __getitem__(self, index):
        return self.data[index]

此时,for item in MySequence([5,6,7]): 仍可正常执行。原因在于 for 循环内部会优先尝试调用 __iter__(),若不存在,则转而调用 __getitem__(),从索引 0 开始依次取值,直至抛出 IndexError 终止。

注意:isinstance(obj, Iterable) 仅识别 __iter__(),不认可 __getitem__(),因此该检测方式对仅实现序列协议的对象存在局限性。


四、迭代器 —— 具备状态保持能力的迭代载体

迭代器在可迭代对象的基础上,额外实现了 __next__() 方法,用于逐一返回下一个元素;当无更多元素时,抛出 StopIteration 异常以终止迭代。

迭代器的核心方法

  • __iter__():返回迭代器自身(迭代器亦为可迭代对象,满足 for 循环的初始调用要求)。
  • __next__():返回下一个有效元素,若无则抛出 StopIteration

自定义迭代器示例(经典模式)

class ContainerIterator:
    def __init__(self, container):
        self.container = container
        self.cursor = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.cursor >= len(self.container.data):
            raise StopIteration
        item = self.container.data[self.cursor]
        self.cursor += 1
        return item

class Container:
    def __init__(self, data):
        self.data = data
    def __iter__(self):
        return ContainerIterator(self)

上述代码允许 for i in Container([4,5,6]): print(i) 正常遍历。其底层执行流程为:

  1. for 语句调用 Container.__iter__() 获取迭代器对象。
  2. 每轮循环调用该迭代器的 __next__() 方法。
  3. 直至 StopIteration 被捕获,循环终止。

手动模拟 for 循环的实现机制

cont = Container([4,5,6])
it = cont.__iter__()
while True:
    try:
        item = it.__next__()
        print(item)
    except StopIteration:
        break

五、生成器 —— 通过 yield 实现惰性求值的迭代器

生成器是构建迭代器最为简洁的方式。开发者无需显式定义 __iter____next__,只需在函数体内使用 yield 关键字返回值,Python 解释器将自动将其编译为生成器对象。

普通函数与生成器函数的对比

# 普通函数:return 终止执行
def normal():
    print("start")
    return 2
    print("end")   # 不可达代码

# 生成器函数:yield 挂起执行
def gen():
    print("start")
    yield 2
    print("continue")
    yield 4
    print("end")

调用 gen() 不会立即执行函数体,而是返回一个生成器对象,该对象是迭代器的子类。

g = gen()
print(isinstance(g, Generator))   # True
print(isinstance(g, Iterator))    # True

执行流程:每次 __next__() 从挂起点恢复

print(g.__next__())   # 输出 "start",返回 2,并挂起
print(g.__next__())   # 输出 "continue",返回 4,并挂起
# 再次调用将抛出 StopIteration,同时输出 "end"(函数体执行完毕)

关键特性:yield 不仅返回一个值,还会保存当前函数的所有局部变量状态,下次调用时从该状态恢复执行。此特性使其在处理需维护状态的遍历场景中具有显著优势。

生成器的遍历方式

for val in gen():
    print(val)   # 输出 2, 4

需要强调的是,同一生成器对象仅能遍历一次,因其内部状态单向推进,无法重置。


六、生成器表达式 —— 内存优化的“惰性”推导式

列表推导式 [i for i in range(5)] 会立即构建完整列表,占用与数据量成正比的内存空间。若数据规模巨大,可采用生成器表达式,将方括号替换为圆括号,从而实现惰性求值。

# 列表推导式 — 立即构建
list1 = [i for i in range(5)]
print(sum(list1))  # 10
print(sum(list1))  # 10(列表可重复使用)

# 生成器表达式 — 惰性求值
gen_exp = (i for i in range(5))
print(sum(gen_exp))  # 10(迭代完成后耗尽)
print(sum(gen_exp))  # 0(因已为空)

生成器表达式尤其适用于一次性消费的大数据场景,例如 sum(i for i in range(10**9)),其内存占用几乎恒定。

提示:若生成器表达式作为函数的唯一参数,外层括号可省略,如 sum(i for i in range(5)),使代码更为简洁。


七、内置工具函数 —— iter()next() 的用法

iter(object[, sentinel]) —— 获取迭代器

  • 单参数形式:对象必须实现 __iter__()__getitem__(),否则引发 TypeError
  • 双参数形式:第一个参数须为可调用对象(如函数),第二个为哨兵值。每次调用该可调用对象,直至返回值等于哨兵时停止迭代。
# 常规用法
it = iter([1,2,3])

# 哨兵用法示例:读取文件直至空行
with open('file.txt') as f:
    for line in iter(f.readline, ''):
        process(line)

next(iterator[, default]) —— 获取下一元素

  • 若迭代器未耗尽,返回下一个元素;若已耗尽且提供了 default,则返回该默认值;否则抛出 StopIteration
it = iter("abc")
print(next(it))   # 'a'
print(next(it))   # 'b'
print(next(it, "end"))  # 'c'(未耗尽,返回实际值)
print(next(it, "end"))  # 'end'(耗尽,返回默认值)

八、迭代器的优势与局限性

优势局限
统一遍历接口,无需依赖索引不支持随机访问(无法直接获取中间元素)
内存高效,每次仅处理一个元素,适合大规模或流式数据单向遍历,不可回退
支持惰性计算,按需生成未知长度(无 len()),除非完全遍历
可被 forlist()sum() 等统一消费一次性使用,耗尽后需重新创建

实践建议

  • 处理大文件、数据流或无限序列时,优先选用生成器或迭代器。
  • 若需重复使用或频繁随机访问,则使用列表、元组等容器类型。

九、三者关系总结(层次结构图)

可迭代对象 (Iterable) 
    ├── 条件:实现 __iter__() 或 __getitem__()
    ├── 实例:list, str, dict, range, ...
    │
    ├── 迭代器 (Iterator)
    │   ├── 条件:实现 __iter__() + __next__()
    │   ├── 实例:reversed, zip, enumerate, 自定义迭代器类
    │   └── 性质:迭代器自身也是可迭代对象(__iter__ 返回自身)
    │
    └── 生成器 (Generator)
        ├── 创建方式:yield 函数 或 生成器表达式
        ├── 性质:生成器是迭代器的子类型,因此也是可迭代对象
        └── 特征:惰性求值、可挂起、内存占用低

结论

所有可被 for 遍历的对象均为可迭代对象;迭代器是具备内部遍历指针的可迭代对象;生成器是能够显式挂起和恢复执行的迭代器,兼具函数式表述与迭代器特性,是处理惰性序列的首选工具。


验证性练习(示例)

def foo():
    for i in range(3):
        yield i

g = foo()
print(list(g))   # ①
print(list(g))   # ②
print(sum(g))    # ③

#	**输出结果**:  
#	① `[0,1,2]`  
#	② `[]`  
#	③ `0`  

解释:生成器对象 g 在第一次 list(g) 时已耗尽,后续调用返回空列表,sum 作用于空迭代器结果为 0。这验证了生成器的单向一次性消费特性。


以上为迭代协议体系的完整论述。掌握这些概念,有助于在数据处理、流式计算及资源敏感型应用中做出合理的技术选型。好了今天的内容就到这儿了如有疑问,欢迎进一步探讨。

下图为本文知识点的思维导图(示意):
在这里插入图片描述

Logo

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

更多推荐