一、生成器的核心概念与本质
1. 前置知识:可迭代对象 vs 迭代器
在讲生成器前,必须先搞懂这两个概念(小白必看):
- 可迭代对象(Iterable) :可以用
for循环遍历的对象,比如列表[1,2,3]、字符串"abc"、字典{"a":1};
判断方法 :用isinstance(obj, Iterable)。 - 迭代器(Iterator) :是可迭代对象的"升级版",可以用
next()逐个获取元素,直到抛出StopIteration异常;
判断方法 :用isinstance(obj, Iterator)。
关系 :迭代器一定是可迭代对象,但可迭代对象不一定是迭代器(比如列表是可迭代对象,但不是迭代器,需要用 iter() 转换成迭代器)。
# 示例:可迭代对象 vs 迭代器
from collections.abc import Iterable, Iterator
my_list = [1, 2, 3] # 可迭代对象
print(isinstance(my_list, Iterable)) # 输出:True
print(isinstance(my_list, Iterator)) # 输出:False
# 用iter()将可迭代对象转换成迭代器
my_iterator = iter(my_list)
print(isinstance(my_iterator, Iterator)) # 输出:True
# 用next()逐个获取元素
print(next(my_iterator)) # 输出:1
print(next(my_iterator)) # 输出:2
print(next(my_iterator)) # 输出:3
# print(next(my_iterator)) # 报错:StopIteration(没有更多元素了)
2. 什么是生成器?
生成器是一种特殊的迭代器 ,它具备迭代器的所有特性(可以用 next()、可以用 for 循环),但创建方式更简单,且惰性求值(按需生成元素,不一次性占用大量内存)。
英语 :Generator
核心特点:
- 不需要一次性生成所有元素,只在需要时生成下一个;
- 极大节省内存,适合处理大数据集或无限序列。
二、生成器的两种创建方式
方式1:生成器表达式(Generator Expression)
1. 语法
与列表推导式几乎一样,只是把 [] 换成 ():
# 列表推导式:一次性生成所有元素,返回列表
list_comp = [i**2 for i in range(5)] # 结果:[0, 1, 4, 9, 16]
# 生成器表达式:按需生成元素,返回生成器对象
gen_comp = (i**2 for i in range(5)) # 结果:<generator object <genexpr> at 0x...>
2. 数学结合例子:生成等比数列的前n项
(高数背景 :等比数列,首项a₁,公比q,通项公式aₙ = a₁×qⁿ⁻¹;英语:Geometric sequence)
a1 = 2 # 首项(First term)
q = 3 # 公比(Common ratio)
n = 5 # 项数(Number of terms)
# 生成器表达式:生成前5项(2, 6, 18, 54, 162)
geometric_gen = (a1 * (q ** i) for i in range(n))
方式2:生成器函数(Generator Function)+ yield 关键字
如果生成器的逻辑比较复杂(需要循环、条件判断、数学计算),就用生成器函数 ,核心是 yield 关键字。
1. yield 与 return 的核心区别(小白必懂)
return:
-
- 执行到
return时,函数立即结束; - 一次性返回所有值(如果有的话);
- 函数执行完毕后,局部变量会被销毁。
- 执行到
yield:
-
- 执行到
yield时,函数暂停执行,保存当前的所有状态(局部变量、执行位置等); - 返回一个值给调用者;
- 下次调用
next()时,函数从暂停的位置继续执行 ,直到遇到下一个yield或函数结束。
- 执行到
2. 生成器函数的基本语法
def 生成器函数名(参数):
初始化代码
while 条件:
yield 要生成的值
更新状态的代码
3. 生成器函数的执行流程(步骤分解,小白必看)
我们用一个简单的例子,逐步骤讲解生成器函数的执行过程:
def count_up_to(n):
"""生成0到n的整数(包含n)"""
print("--- 生成器函数启动 ---")
i = 0
while i <= n:
print(f"--- 准备生成第 {i+1} 个值 ---")
yield i # 暂停点1
print(f"--- 继续执行,更新i为 {i+1} ---")
i += 1
print("--- 生成器函数结束 ---")
现在我们创建生成器对象并调用 next(),逐步骤看发生了什么:
# 步骤1:创建生成器对象(注意:此时函数**不会执行**,只是创建了一个对象)
counter = count_up_to(2)
# 此时没有任何输出,因为函数还没启动
# 步骤2:第一次调用next()
print("第一次调用next()的结果:", next(counter))
# 执行流程:
# 1. 进入count_up_to函数,执行print("--- 生成器函数启动 ---")
# 2. i = 0
# 3. 进入while循环(i=0 <= 2,条件成立)
# 4. 执行print(f"--- 准备生成第 {i+1} 个值 ---")
# 5. 遇到yield i:暂停函数,保存当前状态(i=0,执行位置在yield处),返回i=0给调用者
# 输出:
# --- 生成器函数启动 ---
# --- 准备生成第 1 个值 ---
# 第一次调用next()的结果: 0
# 步骤3:第二次调用next()
print("第二次调用next()的结果:", next(counter))
# 执行流程:
# 1. 从上次暂停的位置(yield i之后)继续执行
# 2. 执行print(f"--- 继续执行,更新i为 {i+1} ---")
# 3. i += 1 → i=1
# 4. 回到while循环开头(i=1 <= 2,条件成立)
# 5. 执行print(f"--- 准备生成第 {i+1} 个值 ---")
# 6. 遇到yield i:暂停函数,返回i=1
# 输出:
# --- 继续执行,更新i为 1 ---
# --- 准备生成第 2 个值 ---
# 第二次调用next()的结果: 1
# 步骤4:第三次调用next()
print("第三次调用next()的结果:", next(counter))
# 执行流程类似,最终返回i=2
# 输出:
# --- 继续执行,更新i为 2 ---
# --- 准备生成第 3 个值 ---
# 第三次调用next()的结果: 2
# 步骤5:第四次调用next()
# print("第四次调用next()的结果:", next(counter))
# 执行流程:
# 1. 从暂停处继续,执行print(f"--- 继续执行,更新i为 {i+1} ---")
# 2. i += 1 → i=3
# 3. 回到while循环开头(i=3 <= 2,条件不成立)
# 4. 执行print("--- 生成器函数结束 ---")
# 5. 函数结束,抛出StopIteration异常
4. 数学结合例子:生成泰勒展开式的前n项(高数应用)
(高数背景 :eˣ的泰勒展开式为 eˣ = 1 + x + x²/2! + x³/3! + ... + xⁿ/n! + ...;英语:Taylor series)
def taylor_exp(x, n_terms):
"""
近似生成e^x的泰勒展开式的前n_terms项
:param x: 自变量x
:param n_terms: 项数
"""
term = 1 # 第0项:x^0 / 0! = 1
yield term # 生成第0项
factorial = 1 # 阶乘,初始为0! = 1
for k in range(1, n_terms):
factorial *= k # 计算k! = (k-1)! × k
term = (x ** k) / factorial # 计算第k项:x^k / k!
yield term # 生成第k项
# 测试:生成e^2的前5项泰勒展开式
x = 2
n_terms = 5
taylor_gen = taylor_exp(x, n_terms)
# 逐个获取项并累加,近似计算e^2
approx = 0
print(f"e^{x}的泰勒展开式前{n_terms}项:")
for i, term in enumerate(taylor_gen):
approx += term
print(f"第{i}项:{term:.6f},当前累加和:{approx:.6f}")
# 实际e^2的值约为7.389056,对比一下
import math
print(f"实际e^{x}的值:{math.exp(x):.6f}")
三、从生成器中获取数据的6种方法(重点)
生成器是迭代器,所以所有迭代器的获取数据方法都适用于生成器,这里详细讲解每一种:
方法1:用 next() 逐个获取(最基础)
-
语法:
next(生成器对象) -
特点:每次调用返回一个元素,直到生成器耗尽,抛出
StopIteration异常 -
适用场景:需要手动控制获取节奏,或只需要前几个元素
示例:用next()获取等比数列的前3项
geometric_gen = (2 * (3 ** i) for i in range(5)) # 前5项:2,6,18,54,162
print(next(geometric_gen)) # 输出:2
print(next(geometric_gen)) # 输出:6
print(next(geometric_gen)) # 输出:18剩下的54和162还没生成,节省内存
方法2:用 for 循环遍历(最常用,推荐)
-
语法:
for 元素 in 生成器对象: -
特点:自动处理
StopIteration异常,不会报错;遍历完后生成器耗尽,无法再次遍历 -
适用场景:需要遍历所有元素
示例:用for循环遍历泰勒展开式生成器
taylor_gen = taylor_exp(2, 5)
print("e^2的泰勒展开式前5项:")
for term in taylor_gen:
print(term)
方法3:转换成列表/元组/集合(一次性获取所有元素)
-
语法:
list(生成器对象)、tuple(生成器对象)、set(生成器对象) -
特点:一次性生成所有元素并转换成对应数据结构;会失去生成器的内存优势(因为所有元素都加载到内存了)
-
适用场景:需要将生成器的结果保存为列表等数据结构,且数据量不大
示例:将生成器转换成列表
geometric_gen = (2 * (3 ** i) for i in range(5))
geometric_list = list(geometric_gen)
print(geometric_list) # 输出:[2, 6, 18, 54, 162]注意:转换成列表后,生成器已经耗尽,无法再次使用
print(next(geometric_gen)) # 报错:StopIteration
方法4:用 next() + 默认值(避免StopIteration异常)
-
语法:
next(生成器对象, 默认值) -
特点:当生成器耗尽时,返回默认值,而不是抛出异常
-
适用场景:不确定生成器是否还有元素,不想处理异常
示例:用next() + 默认值
geometric_gen = (2 * (3 ** i) for i in range(3)) # 只有3项:2,6,18
print(next(geometric_gen, "没有更多元素了")) # 输出:2
print(next(geometric_gen, "没有更多元素了")) # 输出:6
print(next(geometric_gen, "没有更多元素了")) # 输出:18
print(next(geometric_gen, "没有更多元素了")) # 输出:没有更多元素了(不报错)
方法5:用 enumerate() 获取索引和元素(同时知道是第几个元素)
-
语法:
for 索引, 元素 in enumerate(生成器对象): -
特点:可以同时获取元素的索引(从0开始)和元素本身
-
适用场景:需要知道元素的位置
示例:用enumerate()获取斐波那契数列的索引和元素
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + bfib_gen = fibonacci(5)
print("斐波那契数列前5项(带索引):")
for i, num in enumerate(fib_gen):
print(f"第{i}项:{num}")
方法6:用 zip() 同时遍历多个生成器
-
语法:
for 元素1, 元素2 in zip(生成器1, 生成器2): -
特点:同时遍历多个生成器,每次从每个生成器中取一个元素,直到最短的生成器耗尽
-
适用场景:需要同时处理多个序列
示例:用zip()同时遍历两个生成器(生成x和x²的对应关系)
x_gen = (i for i in range(1, 4)) # 生成1,2,3
x_square_gen = (i**2 for i in range(1, 4)) # 生成1,4,9print("x和x²的对应关系:")
for x, x_square in zip(x_gen, x_square_gen):
print(f"{x} → {x_square}")
四、生成器函数的进阶方法:send()/throw()/close()
生成器对象除了 next(),还有三个进阶方法,用于与生成器交互:
1. send():向生成器发送值,并获取下一个元素
- 语法:
生成器对象.send(值) - 作用:
-
- 向生成器暂停的位置发送一个值,这个值会被赋值给
yield左边的变量; - 继续执行生成器,直到遇到下一个
yield或结束; - 返回下一个
yield的值。
- 向生成器暂停的位置发送一个值,这个值会被赋值给
- 注意:第一次调用生成器时,必须先用
next()或send(None)启动生成器 ,不能直接send(非None值),否则会报错。
数学结合例子:生成可调整公差的等差数列
(线代背景 :可以理解为向量的步长调整;英语:Adjustable common difference)
def adjustable_arithmetic(a1, initial_d):
"""
生成可调整公差的等差数列
:param a1: 首项
:param initial_d: 初始公差
"""
current = a1
d = initial_d
while True:
# 暂停,返回current;如果收到send的值,赋值给new_d
new_d = yield current
if new_d is not None:
d = new_d # 更新公差
current += d # 计算下一项
# 测试:生成可调整公差的等差数列
arith_gen = adjustable_arithmetic(a1=1, initial_d=2)
# 第一次必须用next()或send(None)启动
print(next(arith_gen)) # 输出:1(首项)
print(arith_gen.send(None)) # 输出:3(公差2,1+2=3)
# 发送新的公差5
print(arith_gen.send(5)) # 输出:8(公差更新为5,3+5=8)
print(arith_gen.send(None)) # 输出:13(保持公差5,8+5=13)
# 再发送新的公差10
print(arith_gen.send(10)) # 输出:23(公差更新为10,13+10=23)
2. throw():向生成器抛出异常
-
语法:
生成器对象.throw(异常类型, 异常信息) -
作用:在生成器暂停的位置抛出指定的异常,生成器可以捕获这个异常并处理;如果生成器没有捕获,异常会传递给调用者。
-
适用场景:需要在生成器内部处理异常,或强制终止生成器。
示例:用throw()向生成器抛出异常
def count_up_to(n):
i = 0
while i <= n:
try:
yield i
i += 1
except ValueError:
print("生成器捕获到ValueError,继续执行")测试
counter = count_up_to(3)
print(next(counter)) # 输出:0
print(next(counter)) # 输出:1抛出ValueError
counter.throw(ValueError, "这是一个测试异常") # 生成器捕获异常,输出提示
print(next(counter)) # 输出:2(继续执行)
3. close():关闭生成器
-
语法:
生成器对象.close() -
作用:关闭生成器,之后再调用
next()会抛出StopIteration异常;生成器关闭时,会在暂停位置抛出GeneratorExit异常,生成器可以捕获这个异常进行清理工作。 -
适用场景:需要提前终止生成器,释放资源。
示例:用close()关闭生成器
def count_up_to(n):
try:
i = 0
while i <= n:
yield i
i += 1
except GeneratorExit:
print("生成器被关闭,进行清理工作")测试
counter = count_up_to(3)
print(next(counter)) # 输出:0
print(next(counter)) # 输出:1
counter.close() # 关闭生成器,输出清理提示print(next(counter)) # 报错:StopIteration
五、yield from 语法:生成器的嵌套与委托
1. 什么是 yield from?
yield from 是Python 3.3引入的语法,用于委托生成器,即:在一个生成器函数中,调用另一个生成器,并将另一个生成器的所有元素"透传"给调用者。
2. 基本语法
def 外层生成器():
# 委托给内层生成器
yield from 内层生成器()
3. 不用 yield from vs 用 yield from 的对比
不用 yield from:需要手动循环内层生成器
def inner_gen():
"""内层生成器:生成1,2,3"""
yield 1
yield 2
yield 3
def outer_gen():
"""外层生成器:手动循环内层生成器"""
print("外层生成器启动")
for num in inner_gen():
yield num
print("外层生成器结束")
# 测试
for num in outer_gen():
print(num)
# 输出:
# 外层生成器启动
# 1
# 2
# 3
# 外层生成器结束
用 yield from:简化代码,自动透传
def inner_gen():
yield 1
yield 2
yield 3
def outer_gen():
print("外层生成器启动")
yield from inner_gen() # 委托给内层生成器,自动透传所有元素
print("外层生成器结束")
# 测试
for num in outer_gen():
print(num)
# 输出和上面一样,但代码更简洁
4. yield from 的进阶作用:处理嵌套生成器
如果有多层嵌套的生成器,yield from 可以大大简化代码:
# 示例:三层嵌套生成器,用yield from透传
def gen1():
yield 1
yield 2
def gen2():
yield from gen1()
yield 3
yield 4
def gen3():
yield from gen2()
yield 5
# 测试:直接遍历gen3,就能获取所有元素
for num in gen3():
print(num) # 输出:1,2,3,4,5
5. AI结合例子:生成大模型的多轮对话历史
(AI场景 :大模型需要处理多轮对话历史,用 yield from 可以分层生成不同角色的对话;英语:Dialogue history)
def system_prompt():
"""生成系统提示词"""
yield {"role": "system", "content": "你是一个Python教学助手,回答要通俗易懂"}
def user_messages(user_inputs):
"""生成用户对话流"""
for msg in user_inputs:
yield {"role": "user", "content": msg}
def assistant_messages(assistant_outputs):
"""生成助手对话流"""
for msg in assistant_outputs:
yield {"role": "assistant", "content": msg}
def full_dialogue_history(user_inputs, assistant_outputs):
"""拼接完整对话流,惰性生成,不占内存"""
yield from system_prompt()
yield from user_messages(user_inputs)
yield from assistant_messages(assistant_outputs)
# 使用示例
if __name__ == "__main__":
user_inputs = ["你好", "什么是Python生成器?"]
assistant_outputs = ["你好!", "生成器是一种用yield返回的迭代器,可以惰性生成数据..."]
# 流式处理对话历史,内存里只有当前一条数据
print("流式对话历史:")
for turn in full_dialogue_history(user_inputs, assistant_outputs):
print(turn)
六、生成器的核心优势:内存节省与惰性求值
1. 内存对比:列表 vs 生成器(直观展示)
我们用 sys.getsizeof() 来实际测量列表和生成器的内存占用:
import sys
# 生成100万个整数的平方
n = 10**6
# 用列表:一次性生成所有元素
my_list = [i**2 for i in range(n)]
print(f"列表的内存占用:{sys.getsizeof(my_list) / 1024 / 1024:.2f} MB")
# 用生成器:按需生成
my_gen = (i**2 for i in range(n))
print(f"生成器的内存占用:{sys.getsizeof(my_gen)} 字节")
输出结果(不同机器可能略有不同):
列表的内存占用:约8.44 MB
生成器的内存占用:约128 字节
可以看到,生成器的内存占用几乎可以忽略不计,而列表随着数据量增大,内存占用会线性增长。
2. 惰性求值(Lazy Evaluation)
惰性求值是生成器的核心特性,即:只有当调用 next()或 for循环时,才会生成下一个元素,之前的元素如果没有保存,就会被丢弃(除非用列表等保存)。
好处:
- 可以处理无限序列(比如所有正整数、所有斐波那契数),因为不需要一次性生成所有元素;
- 节省内存,适合处理大数据集(比如几GB的文件、AI训练数据)。
例子:生成无限的正整数序列
def infinite_integers():
"""生成无限的正整数序列:1,2,3,4,..."""
i = 1
while True:
yield i
i += 1
# 测试:只获取前5个元素(不会生成无限个,节省内存)
inf_gen = infinite_integers()
for _ in range(5):
print(next(inf_gen)) # 输出:1,2,3,4,5
七、生成器的实战应用(含AI/数学场景)
应用1:大文件读取(避免一次性加载到内存)
如果有一个几GB的日志文件,用列表一次性读取会导致内存溢出,用生成器可以逐行读取:
def read_large_file(file_path):
"""
逐行读取大文件
:param file_path: 文件路径
"""
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip() # 逐行生成,去掉换行符
# 测试:逐行处理大文件(比如统计包含"error"的行数)
error_count = 0
for line in read_large_file("large_log.txt"):
if "error" in line:
error_count += 1
print(f"包含'error'的行数:{error_count}")
应用2:AI大模型的流式文本生成(逐字输出)
(AI场景 :大模型生成文本时,通常是逐字生成的,用生成器可以实现流式输出,让用户实时看到生成的内容;英语:Streaming output)
import time
def llm_text_generator(prompt):
"""
模拟大模型的流式文本生成
:param prompt: 用户输入的提示词
"""
# 模拟大模型生成的文本(实际中是从模型接口获取的)
generated_text = f"你输入的提示词是:{prompt}。生成器非常适合大模型的流式输出,因为它可以逐字生成内容,让用户实时看到结果。"
# 逐字生成(模拟大模型的生成延迟)
for char in generated_text:
time.sleep(0.05) # 模拟生成延迟,每0.05秒生成一个字
yield char
# 测试:模拟大模型的流式输出
prompt = "请介绍生成器在AI中的应用"
print("大模型正在生成:", end="", flush=True)
for char in llm_text_generator(prompt):
print(char, end="", flush=True) # 逐字输出,不换行
print()
应用3:线代中的矩阵逐行生成(处理大矩阵)
(线代背景 :处理大矩阵时,用生成器可以逐行生成,避免一次性占用大量内存;英语:Matrix row generation)
import random
def matrix_generator(rows, cols):
"""
逐行生成一个rows行cols列的随机矩阵
:param rows: 行数
:param cols: 列数
"""
for _ in range(rows):
# 生成一行:cols个0-1之间的随机数
row = [random.random() for _ in range(cols)]
yield row
# 测试:生成一个3行4列的矩阵,并逐行处理
matrix_gen = matrix_generator(3, 4)
print("逐行生成的矩阵:")
for i, row in enumerate(matrix_gen):
print(f"第{i}行:{[round(x, 2) for x in row]}") # 保留两位小数
应用4:高数中的无限级数求和(按需计算项数)
(高数背景 :无限级数求和时,用生成器可以按需生成项,直到达到精度要求;英语:Infinite series summation)
def harmonic_series():
"""生成调和级数的项:1/1, 1/2, 1/3, 1/4, ..."""
n = 1
while True:
yield 1 / n
n += 1
# 测试:计算调和级数的前n项和,直到和超过5
harmonic_gen = harmonic_series()
sum_harmonic = 0
n_terms = 0
while sum_harmonic <= 5:
sum_harmonic += next(harmonic_gen)
n_terms += 1
print(f"调和级数前{n_terms}项和超过5,和为:{sum_harmonic:.6f}")
应用5:自定义数据加载器, 分批次获取数据, 模拟: 后续分批次训练模型.
import math
# 需求: 已知文件中记录的是所有的歌词数据, 请自定义数据加载器, 接收 条数/批次, 分批次获取数据.
# todo 1.定义函数, 获取所有批次的数据 -> 封装到 生成器对象中.
def get_dataloader(batch_size):
"""
该函数用于 按照指定的批次大小, 分批次获取数据, 并封装到 生成器对象中.
:param batch_size: 批次大小, 例如: 8条/批
:return: 生成器对象
"""
# 1. 一次性从文件中读取所有的数据, 并封装到列表中.
with open('./data/jaychou_lyrics.txt', 'r', encoding='utf-8') as src_f:
lines = src_f.readlines() # 格式为: ['第1行\n', '第2行\n', '第3行\n', ...]
# 2. 获取总数据条数.
total_size = len(lines) # 5819条
# 3. 计算总批次数.
# 思路1: 调用 math模块的ceil()函数, 简写为: math#ceil()
# total_batch = math.ceil(total_size / batch_size)
# print(total_batch)
# 思路2: 自己推导公式, 总批次数 = (数据总数 + 每批次的数据条数 - 1) // 每批次的数据条数
total_batch = (total_size + batch_size - 1) // batch_size # 728批
# 4. 遍历, 具体的获取每一批次数据, 然后封装到生成器中.
for i in range(total_batch): # i是批次索引, 从0开始 -> 第1批, 1 -> 第2批...
# 思考: 每批次的数据具体如何获取?
# 第0批次: lines[0:8]
# 第1批次: lines[8:16]
# 第i批次: lines[i * batch_size: i * batch_size + batch_size]
yield lines[i * batch_size: i * batch_size + batch_size] # yield三件事: 创建, 添加, 返回
# todo 2.调用函数.
my_generator = get_dataloader(8)
print(my_generator, type(my_generator)) # <class 'generator'>
print('-' * 30)
# 获取第1批次的数据.
print(next(my_generator))
# 获取第2批次的数据.
print(next(my_generator))
学习总结
- 生成器的本质:特殊的迭代器,惰性求值,节省内存;
- 两种创建方式:
-
- 简单逻辑:生成器表达式
(...); - 复杂逻辑:生成器函数 +
yield;
- 简单逻辑:生成器表达式
- 获取数据的6种方法 :
next()、for循环、转换成列表/元组、next(, 默认值)、enumerate()、zip(); - 进阶方法 :
send()(传值)、throw()(抛异常)、close()(关闭); yield from:委托生成器,简化嵌套;- 实战应用:大文件读取、AI流式输出、矩阵生成、无限级数求和。