迭代器(Iterator)和生成器(Generator)
迭代器和生成器的关系:生成器是迭代器的“快捷方式”。你不需要写繁琐的类和 __next__ 方法,只需要用一个特殊的关键字:yield。
是什么:简单来说,迭代器(Iterator) 是让你能一个接一个访问元素的对象,而生成器(Generator) 则是创建这些迭代器的一种更优雅、更省内存的高级方式。
作用:节省内存。如果你有一个包含 1 亿个数字的列表,它会瞬间吃光你的 RAM。但迭代器不需要在内存中存储所有数字,它只记住当前的位置,每次调用时计算出下一个值。
它们都采用 “惰性求值”(Lazy Evaluation),只有在你要的时候才计算。
代码实现:迭代器
class MyCounter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
# 迭代器协议要求返回对象本身
return self
def __next__(self):
# 每次调用 next() 都会跑这里的逻辑
if self.current <= self.high:
value = self.current
self.current += 1
return value
else:
# 没有数据了,必须抛出这个异常,否则 for 循环不会停止
raise StopIteration
# 使用迭代器
counter = MyCounter(1, 3)
print(next(counter)) # 输出 1
print(next(counter)) # 输出 2
print(next(counter)) # 输出 3
# print(next(counter)) # 再次调用会触发 StopIteration
代码实现:生成器函数
场景:实现斐波那契数列(Fibonacci) 这是一个经典的案例,因为斐波那契数列可以是无限的,用生成器处理最节省内存。
生成器用 yield 代替了繁琐的类定义。它会自动帮我们处理 __iter__、__next__ 和 StopIteration。
def fibonacci_gen(n):
"""生成前 n 个斐波那契数"""
a, b = 0, 1
count = 0
while count < n:
yield a # 产出值,并在此处“暂停”
a, b = b, a + b
count += 1
# 使用生成器
fib = fibonacci_gen(5)
print(type(fib)) # <class 'generator'>
for num in fib:
print(num) # 依次输出 0, 1, 1, 2, 3
代码讲解
代码中没有return,它是怎么返回值的?难道是yield返回了值吗?
是的。yield 的确返回了值,但它和 return 的工作方式完全不同。
return:直接返回所有结果,如果数据量巨大内存将会爆炸。
yield:只有当你需要值的时候才会返回,比如for循环或者手动调用next。
代码执行的微观过程:
让我们看看你的代码在 for num in fib 时发生了什么:
1、第一次循环: 进入函数,a=0, b=1。碰到 yield 0。函数暂停,把 0 传给外面的 num。打印 0。
2、第二次循环: 从 yield a 的下一行开始:计算 a, b = 1, 1,count = 1。回到循环顶部,碰到 yield 1。函数暂停,把 1 传给 num。打印 1。
3、以此类推...
4、最后: 当 count < n 不再成立时,函数自然结束。for 循环感知到函数结束了,就会优雅地停止,而不会报错。
为什么要这么麻烦?(核心优势):
如果我们要生成前 100 万个斐波那契数:
用 return: 你必须先创建一个包含 100 万个数字的列表,这会瞬间吃掉几百 MB 的内存。
用 yield: 内存中始终只保存 a, b, count 这三个变量。无论生成多少个数,内存占用几乎为零。
yield和return对比
return vs yield 深度对比
| 特性 | return (普通函数) | yield (生成器函数) |
|---|---|---|
| 执行状态 | 执行到此行立即终止函数。 | 执行到此行暂停函数,保留现场。 |
| 返回值 | 返回一个具体的值或对象。 | 返回一个生成器对象(迭代器)。 |
| 内存占用 | 必须一次性生成所有结果(大列表)。 | 按需生成,一次只占一个元素的内存。 |
| 变量生命周期 | 函数退出后,局部变量被销毁。 | 局部变量状态被封存,下次从断点继续。 |
| 调用方式 | 调用一次执行一次完整逻辑。 | 配合 for 循环或 next() 多次获取值。 |
生成器表达式和列表推导式
生成器最“轻量化”的形式:生成器表达式(Generator Expression) 。
它和列表推导式(List Comprehension)长得几乎一模一样,唯一的区别是:列表推导式用方括号 [],而生成器表达式用圆括号 ()。
但在内存的世界里,这一个小小的括号差别,就是“倾家荡产”与“勤俭持家”的区别。
1、内存大比拼:[] vs ()
假设我们要处理 100 万个 数字的平方:
import sys
# 1. 列表推导式:直接在内存里创建 100 万个元素的列表
list_comp = [x**2 for x in range(1000000)]
print(f"列表占用内存: {sys.getsizeof(list_comp)} 字节")
# 2. 生成器表达式:只创建了一个“生成规则”
gen_exp = (x**2 for x in range(1000000))
print(f"生成器占用内存: {sys.getsizeof(gen_exp)} 字节")
内存占用: 列表推导式:8448728 字节(约8MB) 生成器表达式:208 字节
既然你感兴趣,那我们就来看看生成器最“轻量化”的形式:生成器表达式(Generator Expression) 。
它和列表推导式(List Comprehension)长得几乎一模一样,唯一的区别是:列表推导式用方括号 [],而生成器表达式用圆括号 ()。
但在内存的世界里,这一个小小的括号差别,就是“倾家荡产”与“勤俭持家”的区别。
1. 内存大比拼:[] vs ()
假设我们要处理 100 万个 数字的平方:
import sys
# 1. 列表推导式:直接在内存里创建 100 万个元素的列表
list_comp = [x**2 for x in range(1000000)]
print(f"列表占用内存: {sys.getsizeof(list_comp)} 字节")
# 2. 生成器表达式:只创建了一个“生成规则”
gen_exp = (x**2 for x in range(1000000))
print(f"生成器占用内存: {sys.getsizeof(gen_exp)} 字节")
结果对比:
| 方式 | 内存占用(典型值) | 状态 |
|---|---|---|
列表 [ ] | 约 8,000,000 字节 (8MB) | 瞬间塞满内存,数据都在那儿。 |
生成器 ( ) | 约 100 字节 | 几乎不占内存,数据“随叫随到”。 |
2. 为什么生成器表达式这么神奇?
生成器表达式本质上是 yield 函数的缩写版。
当你写下 (x**2 for x in range(10)) 时,Python 在后台其实为你做了一个类似这样的工作:
def __hidden_generator():
for x in range(10):
yield x**2
gen_exp = __hidden_generator()
3. 什么时候用生成器表达式?
-
数据处理管道:比如读取一个巨大的 CSV 文件,过滤掉无效行,然后转换数据。你可以用一连串的生成器表达式,数据会像流水一样流过这些“滤芯”,而不会堵塞内存。
-
聚合运算:如果你只是想算个总和
sum()、最大值max(),完全没必要先存成列表。
# 优雅且省内存的写法
total_sum = sum(x**2 for x in range(1000000))
可迭代对象
1. 是什么?
是什么:可以使用for遍历的对象就是可迭代对象。
实现:简单来说,只要一个对象实现了 __iter__() 魔术方法(或者实现了 __getitem__() 且支持整数索引),它就是可迭代对象。
这意味着它承诺了: “你可以用 for 循环来遍历我。”
常见的可迭代对象:
- 序列类型:
list(列表),str(字符串),tuple(元组),bytes - 非序列类型:
dict(字典),set(集合) - 文件对象
- 生成器
2. 可迭代对象 vs 迭代器
这是最容易混淆的地方。请记住这个公式:
可迭代对象 (Iterable) 迭代器 (Iterator)
| 特性 | 可迭代对象 (Iterable) | 迭代器 (Iterator) |
|---|---|---|
| 魔法方法 | 只有 __iter__ | 必须有 __iter__ 和 __next__ |
| 能否 next() | 不能直接调用 next() | 可以调用 next() |
| 例子 | [1, 2, 3] | iter([1, 2, 3]) |
通过 iter() 函数把可迭代对象变成迭代器
如上公式,可以使用 iter() 函数把可迭代对象变成迭代器。
my_list = [10, 20]
# 1. 获取迭代器
it = iter(my_list)
# 2. 模拟循环
try:
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 这里会抛出 StopIteration
except StopIteration:
print("遍历结束!")
这里模拟for的过程,如果再在代码中加入一个for的话,那么就会跳过奇数个输出,因为for遍历一个,next又遍历了一个。
这也就是为什么不让 List 直接当 Iterator的原因:
- 如果你让 List 自带
next(),那么当你进行嵌套循环(如双重for循环)时,内层循环会直接消耗掉外层循环的进度,导致逻辑混乱。 - 通过
iter()产生独立的迭代器,意味着你可以对同一个列表同时开启多个独立的遍历进度。
可迭代对象、迭代器、生成器
在 Python 的世界里,关系是这样的:
- 可迭代对象 (Iterable) :比如
list,str,你可以对它用for循环。 - 迭代器 (Iterator) :是一个有“车头”的对象,你调用
next()它就走一步。 - 生成器 (Generator) :是一种特殊的迭代器。所有的生成器都是迭代器,但迭代器不一定是生成器。
生成器(Generator)是迭代器(Iterator)的“语法糖”和进化版。在 95% 的实际开发场景中,我们都会优先使用生成器。
要实现一个标准的迭代器,你必须手动写一个类,并实现两个特定方法:
__iter__(): 返回对象本身。__next__(): 决定下一次给什么值,并在结束时手动抛出StopIteration异常。
而生成器只需要一个带有 yield 的普通函数。