当前位置: 首页 > news >正文

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 的作用是:

  1. return 一样,它也会“产出”一个值。
  2. 但它不会结束函数!函数会在这里“暂停”,像按了暂停键一样,记住它停在哪儿了,以及当时所有变量的状态。
  3. 等着下次被“唤醒”时,它会从上次暂停的地方继续往下执行。

听起来有点神奇?我们来看个例子。

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 在默默工作呢!

相关文章:

  • 基于统计方法的水文数据分析及AI拓展
  • windows中搭建Ubuntu子系统
  • [极客大挑战 2019]Upload
  • redis单机安装
  • 智能指针之设计模式1
  • Vue--常用组件解析
  • C#容器源码分析 --- Dictionary<TKey,TValue>
  • 测试复习题目(1)
  • 论文学习:《通过基于元学习的图变换探索冷启动场景下的药物-靶标相互作用预测》
  • C++基础精讲-05
  • 4G/5G模组----概念+驱动+调试
  • 《轨道力学导论》——第十章:前沿轨道理论与应用
  • Java常用连接池 (HikariCP, Tomcat Pool, Druid) 的配置和比较
  • Nginx代理Minio出现AccessDeniedAccessDenied
  • 软件生命周期模型:瀑布模型、螺旋模型、迭代模型、敏捷开发、增量模型、快速原型模型
  • JUC并发工具
  • ARM裸机开发——交叉编译器
  • IS-IS中特殊字段——OL过载
  • 大概解释一下:极值统计理论(Extreme Value Theory, EVT)
  • 【时频谱分析】小波分析
  • AI创业者聊大模型应用趋势:可用性和用户需求是关键
  • 贯彻落实《生态环境保护督察工作条例》,充分发挥生态环境保护督察利剑作用
  • 人民日报头版:紧盯“学查改”,推动作风建设走深走实
  • 竞彩湃|英超欧冠悬念持续,纽卡斯尔诺丁汉能否拿分?
  • 商务部:对原产于美国、欧盟、台湾地区和日本的进口共聚聚甲醛征收反倾销税
  • 倒票“黄牛”屡禁不绝怎么破?业内:强化文旅市场票务公开制度