Python yield关键字
你有没有见过像 ChatGPT 这样的 AI 回答问题时,文字不是一下子全出来,而是一段一段、甚至一个词一个词地蹦出来?这种像打字一样的效果,叫做“流式响应”。在 Python 程序里,要实现这种效果,经常会用到一个特别的关键字:yield
。
yield
这个词看起来有点陌生,但它其实是 Python 中一个非常巧妙的设计。要理解它,我们先得把它和大家熟悉的 return
做个对比。
在一个普通的 Python 函数里,return
语句表示函数的终点。一旦执行 return
,函数就彻底结束了,并且把 return
后面的值交出去。比如:
def add_numbers(a, b):
print("开始计算...")
result = a + b
print("计算完成!")
return result # 函数在这里结束,返回 result
sum_value = add_numbers(5, 3)
print(f"得到结果: {sum_value}")
# 输出:
# 开始计算...
# 计算完成!
# 得到结果: 8
这个函数从头跑到尾,执行 return
后就“寿终正寝”了。
但是,如果函数里用了 yield
,事情就变得不一样了。包含 yield
的函数不再是一个普通函数,它变成了一个“生成器函数”。yield
的作用是:
- 像
return
一样,它也会“产出”一个值。 - 但它不会结束函数!函数会在这里“暂停”,像按了暂停键一样,记住它停在哪儿了,以及当时所有变量的状态。
- 等着下次被“唤醒”时,它会从上次暂停的地方继续往下执行。
听起来有点神奇?我们来看个例子。
def simple_counter(max_num):
print("计数器启动!")
n = 0
while n < max_num:
print(f"准备产出 {n}...")
yield n # 产出 n,然后暂停在这里!
print(f"刚产出了 {n},继续...")
n += 1
print("计数器结束。")
# 调用生成器函数,不会马上执行,而是得到一个“生成器对象”
my_gen = simple_counter(3)
print(f"得到生成器对象: {my_gen}") # 这时函数体内的代码还没执行
print("\n第一次请求值:")
value1 = next(my_gen) # 用 next() 唤醒生成器,执行到第一个 yield
print(f"收到值: {value1}")
print("\n第二次请求值:")
value2 = next(my_gen) # 从上次暂停处继续,执行到第二个 yield
print(f"收到值: {value2}")
print("\n第三次请求值:")
value3 = next(my_gen) # 再次从暂停处继续,执行到第三个 yield
print(f"收到值: {value3}")
# 如果我们再用 next(my_gen),因为循环结束,函数会执行完并抛出 StopIteration 异常
# 通常我们用 for 循环更方便,它会自动处理这个结束信号
print("\n用 for 循环遍历生成器:")
# 重新创建一个生成器对象来演示 for 循环
for number in simple_counter(2): # for 循环会自动调用 next()
print(f"循环拿到: {number}")
输出会是这样的:
得到生成器对象: <generator object simple_counter at 0x...>
第一次请求值:
计数器启动!
准备产出 0...
收到值: 0
第二次请求值:
刚产出了 0,继续...
准备产出 1...
收到值: 1
第三次请求值:
刚产出了 1,继续...
准备产出 2...
收到值: 2
用 for 循环遍历生成器:
计数器启动!
准备产出 0...
循环拿到: 0
刚产出了 0,继续...
准备产出 1...
循环拿到: 1
刚产出了 1,继续...
计数器结束。
看到了吗?函数 simple_counter
并不是一口气执行完的。每次我们通过 next()
或 for
循环向它要值时,它才执行一小段,直到遇到 yield
,就把值交出来然后暂停。下次再要,它就从暂停的地方接着跑。
yield
有什么好处呢?
最大的好处是节省内存和实现惰性计算。想象一下,你要处理一个超级大的文件,或者生成一个包含一百万个元素的序列。
如果用传统方法,比如创建一个巨大的列表:
def create_big_list(count):
# 想象这里要存储一百万个数字
print("开始创建大列表...")
result = []
for i in range(count):
result.append(i * i) # 所有的结果都算出来存到列表里
print("大列表创建完毕!")
return result
# big_list = create_big_list(1_000_000) # 这会瞬间占用大量内存!
# print("列表创建完成")
这需要一次性把所有结果都计算出来并放在内存里,数据量一大,内存可能就爆了。
但如果用 yield
:
def generate_big_squares(count):
print("生成器启动...")
for i in range(count):
# 只有当你需要下一个值时,这里才计算
print(f"正在计算 {i} 的平方...")
yield i * i # 产出一个就暂停,不保存所有结果
print("生成器结束.")
# 创建生成器对象,几乎不占内存,计算也还没开始
squares_gen = generate_big_squares(5) # 试试改成 1_000_000,也不会有问题
print("开始迭代获取值:")
for square in squares_gen: # 每次循环才驱动生成器计算下一个值
print(f" 得到一个平方值: {square}")
if square > 10: # 可以随时停止处理
print(" 值够大了,停止处理。")
break
输出:
生成器启动...
开始迭代获取值:
正在计算 0 的平方...
得到一个平方值: 0
正在计算 1 的平方...
得到一个平方值: 1
正在计算 2 的平方...
得到一个平方值: 4
正在计算 3 的平方...
得到一个平方值: 9
正在计算 4 的平方...
得到一个平方值: 16
值够大了,停止处理。
yield
方式是“要一个,给一个”,它只在需要的时候才计算和生成数据,并且每次只占用当前计算所需的少量内存。这就是所谓的惰性计算。
现在回到 LLM 的流式响应。
LLM 生成一大段文本是需要时间的。如果等它全部生成完再显示,用户就得干等着,体验不好。
流式响应就是,LLM 每生成一小部分(可能是一个词,一句话),就立刻把它发送出来。接收方(比如我们的 Python 后端程序)收到这一小部分后,不是囤着,而是立刻再把它发给最终用户(比如网页)。
yield
在这里扮演了关键角色。我们可以写一个 Python 函数(生成器函数)来处理从 LLM 过来的数据流:
import time
import random
# 这是一个模拟,实际中会是从网络接收 LLM 数据
def llm_response_stream_simulator(prompt):
print(f"模拟 LLM 收到提示: '{prompt}'")
full_response = ["你好!", "我是一个", "大型语言模型。", "很高兴", "为您服务。"]
for chunk in full_response:
# 模拟 LLM 生成每个片段需要的时间
delay = random.uniform(0.2, 0.8)
print(f" (模拟 LLM 思考 {delay:.2f} 秒...)")
time.sleep(delay)
print(f" 模拟 LLM 生成了片段: '{chunk}'")
yield chunk # 关键!拿到一小块,立刻通过 yield 发出去
print("模拟 LLM 响应结束。")
# 使用这个生成器
print("向 LLM 请求...")
response_generator = llm_response_stream_simulator("打个招呼")
print("\n开始接收并显示流式响应:")
all_text = ""
for text_chunk in response_generator: # 每次循环,驱动模拟器生成并 yield 下一块
print(f" >> 前端收到: {text_chunk}")
all_text += text_chunk # 逐渐拼凑完整响应
print("\n流式响应接收完毕!")
print(f"完整响应是: '{all_text}'")
输出(每次运行延迟会不同):
向 LLM 请求...
得到生成器对象: <generator object llm_response_stream_simulator at 0x...>
开始接收并显示流式响应:
模拟 LLM 收到提示: '打个招呼'
(模拟 LLM 思考 0.45 秒...)
模拟 LLM 生成了片段: '你好!'
>> 前端收到: 你好!
(模拟 LLM 思考 0.62 秒...)
模拟 LLM 生成了片段: '我是一个'
>> 前端收到: 我是一个
(模拟 LLM 思考 0.28 秒...)
模拟 LLM 生成了片段: '大型语言模型。'
>> 前端收到: 大型语言模型。
(模拟 LLM 思考 0.71 秒...)
模拟 LLM 生成了片段: '很高兴'
>> 前端收到: 很高兴
(模拟 LLM 思考 0.33 秒...)
模拟 LLM 生成了片段: '为您服务。'
>> 前端收到: 为您服务。
模拟 LLM 响应结束。
流式响应接收完毕!
完整响应是: '你好!我是一个大型语言模型。很高兴为您服务。'
在这个模拟里,llm_response_stream_simulator
函数就像是接收 LLM 数据并处理的后端逻辑。它每收到(或模拟生成)一小块文本 chunk
,就立刻用 yield chunk
把它“产出”。外面的 for
循环则模拟了接收并显示这些文本块的过程。这样一来,用户就能看到文字一点点出现,而不是等待最后的结果。
总结一下:
Python 的 yield
关键字能让函数变成一个可以暂停和恢复的生成器。这使得我们可以写出内存效率极高的代码来处理大量数据,并且非常适合实现像 LLM 那样的流式数据处理,因为它允许我们“来多少,处理多少,传递多少”,从而给用户带来更流畅、更即时的体验。下次再看到那种“打字机”效果,你就可以想到,背后很可能就是 yield
在默默工作呢!