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

Python基础之表达式:yield

一、介绍

网上有许多文章描述yield的诞生是为生成器而设计,其实这种论述不完全正确。Python设计理念中有一项是想让程序执行状态对象化,能够控制执行状态可暂停、可恢复、可逐步推进,而yield就是其实现的核心,生成器是这种理念的一种产物。

二、目标与身份

1. 目标

为解决 “惰性迭代”与“协程”两大核心问题,Python引入yield的核心目标有两个:

  • 初级目标(生成器):解决“海量数据迭代的内存问题”—— 无需一次性生成所有数据,而是按需生成(惰性迭代),这是生成器的核心价值;
  • 高级目标(协程):让函数具备 “暂停 / 恢复 / 双向通信” 能力,实现轻量级并发(执行状态对象化)—— 这是yield作为表达式的设计初衷,也是后来async/await的前身。

2. 关键字和表达式

yield的身份具有双重性,这里理解的关键:

  1. 作为关键字,定义生成器的核心标识
    • 当yield以语句形式使用(如yield 1),它是关键字,这是最基础的用法,用于标记生成器函数的 “暂停点”。
    • 语法特征:单独占据一行(或行尾),作用是 “返回值并暂停函数”,此时yield是定义生成器的核心关键字。
  2. 作为表达式,支持双向数据传递(进阶用法)
    • 当yield出现在赋值语句右侧(如x = yield 1),它是表达式, 这是yield的进阶形态,支持向生成器 “发送数据”,也是协程的基础;
    • 语法特征:yield作为表达式可被赋值、参与计算,示例:
    # yield作为表达式:接收外部发送的数据defgen_coro():print("初始化生成器")x=yield1# yield是表达式,等待接收外部数据并赋值给xprint(f"接收到外部数据:{x}")yield2gen=gen_coro()# 第一步:执行到x = yield 1,返回1,暂停print(next(gen))# 初始化生成器 → 1# 第二步:向生成器发送数据100,赋值给x,继续执行print(gen.send(100))# 接收到外部数据:100 → 2

三、执行语义

1. 先看普通函数

# 定义函数deff():a,b=1,2returna+b# 调用函数f()

解释器的行为流程如下:

  1. 根据def语句创建函数对象(该函数对象内部关联了一个代码对象);
  2. 调用函数时,创建一个新的帧对象,即“执行帧”;
  3. 从函数体起始位置开始顺序执行指令;
  4. 遇到 return 语句:
    • 计算返回值
    • 销毁执行帧
    • 将结果返回给调用方

普通函数的执行帧是一次性的。一旦retur 被执行,该次执行过程即告结束,其执行状态不会被保留,也不可能被恢复。

2. yield对函数调用语义的根本影响

一旦函数体中出现yield表达式,解释器在语法分析阶段就会将该函数判定为生成器函数。这一判定结果会直接影响该函数在调用阶段的语义:

  • 调用普通函数 → 立即创建执行帧并执行函数体;
  • 调用生成器函数 → 返回一个生成器对象,而不立即执行函数体;

也就是说,yield改变的并不是函数体中的某一步行为,而是从定义层面改变了函数调用的执行模型与返回结果类型。
从语言设计角度看,yield 的引入为了允许:函数的执行过程本身成为一个可被拆解、可被对象化、可被外部逐步驱动的运行期实体,简化循环或节省内存在这个过程中也实现了。

3. 普通函数与生成器函数:定义层与执行层的差异

定义层面看,Python 中的函数可以分为两类:

  • 普通函数:函数体中不包含 yield;
  • 生成器函数:函数体中至少包含一条 yield 表达式;
    这一差异在 def 语句执行时即已确定,并体现在函数对象所关联的代码对象上。

两类函数在调用语义上的根本区别在于:

  • 调用普通函数时,解释器立即创建执行帧并执行代码;
  • 调用生成器函数时,解释器返回一个生成器对象,而暂不创建执行帧
    生成器对象可以被理解为对生成器函数一次潜在执行过程的封装。它本身并不等同于执行帧,而是一个运行期控制对象,用于在需要时创建、保存并恢复执行帧。

4. yield的核心语义:冻结当前执行帧

对于包含yield的函数而言,def语句仅创建函数对象调用该函数时才返回生成器对象
执行帧的创建与激活,则发生在生成器对象被首次推进时(如 next() 或 send(None))

deffn():x=1yieldx x+=1yieldx+1

当执行gen = fn() next(gen)时,解释器的执行过程为:

  1. 调用fn(),返回生成器对象;
  2. 调用 next(gen):
    → 创建并激活执行帧;
    → 从函数体起始位置开始执行;
  3. 执行至 yield x:
    → 对表达式 x 求值;
    → 将该值交还给调用方;
    → 冻结当前执行帧的状态(包括指令位置、局部变量绑定及相关执行上下文);
    这里所谓的冻结,指的是执行帧的暂停。即函数的执行过程尚未完成,其执行状态被完整保留下来,等待后续恢复。

5. 恢复执行与执行终止:yield与return的语义分野

当生成器对象被多次推进时,解释器不会重新调用生成器函数,也不会重新创建执行帧。

g=fn()print(type(g))# <class 'generator'>print(next(g))# 1print(next(g))# 3print(next(g))# throw StopIteration

执行过程是:

  1. 第一次 next(gen):
    → 创建并激活执行帧;
    → 执行至第一个 yield,返回yield表达式的值;
    → 然后冻结执行帧;
  2. 第二次 next(gen):
    → 恢复此前冻结的执行帧,从上一次 yield 之后继续执行;
    → 执行至第二个 yield,返回此 yield 表达式的值;
  3. 第三次 next(gen):
    → 恢复冻结的执行帧,从第二次 yield 之后继续执行;
    → 已到生成器函数底部,无法继续推进,抛出StopIteration,销毁执行帧;

整个过程中,推进的始终是同一个执行帧。由此可以清晰地区分 yield 与 return 的本质差异:

  1. yield表示执行过程的暂停点;
    • 执行帧被保留;
    • 执行状态可恢复;
  2. return表示执行过程的终止点;
    • 执行帧被销毁
    • 执行状态不可恢复;
      当生成器函数运行至末尾,或在生成器函数内显式执行return时:
  • 解释器向调用方抛出 StopIteration
  • 执行帧被销毁
  • 生成器对象进入终止态
    因此,两者的区别并不在于是否“返回值”,而在于是否保留并允许继续推进当前执行帧。

6. yield作为表达式:值的双向流动与send机制

绝大多数Python表达式的值,均在其执行当下立即确定,但yield是一个例外。
yield表达式的产出值在执行当下确定,而该表达式的求值结果则延迟到下一次恢复执行时,由外部注入。

deffn():x=yield1# yield是表达式,等待接收外部数据并赋值给xyieldx g=fn()# 创建生成器对象,函数未执行,仅初始化状态print(type(g))# <class 'generator'>print(next(g))# 1print(g.send("hello"))# helloprint(next(g))# throw StopIteration

从执行结果来看,可得出以下规则:

  • yield作为表达式时,x = yield 1分为两步:① 执行yield 1返回值并暂停;② 下次恢复执行时,将外部传入的值赋值给x。
  • send(value)的作用:① 向生成器发送value,作为当前暂停处yield表达式的返回值;② 触发生成器继续执行,直到遇到下一个yield或函数结束。
  • next(g)等价于g.send(None):仅恢复执行,不发送数据。

四、基础示例

1. 对比普通函数和生成器函数

# ❶ 普通函数(return,执行一次就结束)defnormal_func():print("执行第一步")return1print("执行第二步")# 不会执行res=normal_func()print(res)# 执行第一步 → 1# ❷ 生成器函数(yield,可暂停恢复)defgen_func():print("执行第一步")yield1# 返回1,暂停print("执行第二步")yield2# 返回2,暂停print("执行第三步")yield3# 返回3,暂停print("执行结束")# 创建生成器对象(函数未执行,只是准备)gen=gen_func()print(gen)# <generator object gen_func at 0x...>(生成器对象)# 第一次调用next():执行到第一个yieldprint(next(gen))# 执行第一步 → 1# 第二次调用next():从暂停处继续,到第二个yieldprint(next(gen))# 执行第二步 → 2# 第三次调用next():从暂停处继续,到第三个yieldprint(next(gen))# 执行第三步 → 3# 第四次调用next():函数执行完毕,抛出StopIteration# print(next(gen)) # 报错:StopIteration

2. 生成器的遍历

生成器是可迭代对象,无需手动调用next(),直接用for循环遍历即可(自动处理StopIteration):

defgen_func():yield1yield2yield3# for循环遍历生成器(最常用)fornumingen_func():print(num)# 1 → 2 → 3# 转为列表(一次性获取所有值,失去惰性优势)lst=list(gen_func())print(lst)# [1,2,3]

五、核心用法

yield的核心优势是惰性求值+低内存占用,以下是开发中最常用的场景:

1. 生成无限序列(普通列表做不到)

# 场景:生成无限自然数序列(普通列表会内存溢出)definfinite_nums():n=1whileTrue:yieldn n+=1# 创建生成器(不占用内存)nums=infinite_nums()# 取前5个值(按需生成,不会死循环)for_inrange(5):print(next(nums))# 1 → 2 → 3 → 4 → 5

2. 处理大数据集(避免内存爆炸)

# 场景:读取超大文件(10GB),逐行处理(普通readlines会加载全部内容到内存)defread_big_file(file_path):withopen(file_path,"r",encoding="utf-8")asf:forlineinf:yieldline.strip()# 逐行返回,仅占用当前行的内存# 遍历处理(每次只加载一行)forlineinread_big_file("big_file.txt"):print(line)# 处理逻辑

3. 替代列表推导式(生成器表达式,更省内存)

生成器表达式是yield的简化写法,语法和列表推导式类似(把[]换成()):

# ❶ 列表推导式(一次性生成所有值,占内存)lst=[x*xforxinrange(1000000)]# 占用大量内存# ❷ 生成器表达式(惰性求值,几乎不占内存)gen=(x*xforxinrange(1000000))# 遍历生成器,按需计算平方值fornumingen:ifnum>1000:breakprint(num)

4. 协程/异步编程(yield的进阶用法)

yield还能实现简单的协程(Python 3.5+推荐用async/await,但yield是基础):

# 场景:两个任务交替执行(协程雏形)deftask1():foriinrange(3):print(f"任务1:{i}")yield# 暂停,交出执行权deftask2():foriinrange(3):print(f"任务2:{i}")yield# 暂停,交出执行权# 交替执行两个任务t1=task1()t2=task2()whileTrue:try:next(t1)next(t2)exceptStopIteration:break# 输出:# 任务1:0 → 任务2:0 → 任务1:1 → 任务2:1 → 任务1:2 → 任务2:2

5. 带返回值的生成器(Python 3.3+)

生成器函数可以用return指定最终返回值(遍历结束后通过StopIterationvalue获取):

defgen_func():yield1yield2return"生成器执行完毕"gen=gen_func()try:whileTrue:print(next(gen))exceptStopIterationase:print(e.value)# 生成器执行完毕

六、注意事项

1. 生成器只能遍历一次

gen=(xforxinrange(3))# 第一次遍历:正常输出print(list(gen))# [0,1,2]# 第二次遍历:空列表(生成器已耗尽)print(list(gen))# []# 解决:重新创建生成器对象 → gen = (x for x in range(3))

2. 混淆生成器函数和普通函数的返回值

  • ❌ 错误:res = gen_func()(res是生成器对象,不是yield返回的值);
  • ✅ 正确:res = next(gen_func())for res in gen_func()

3. 生成器中的异常处理

生成器执行过程中抛出的异常会中断迭代,需提前处理:

defgen_func():yield1raiseValueError("自定义异常")yield2gen=gen_func()print(next(gen))# 1# next(gen) # 报错:ValueError: 自定义异常# 解决:遍历时报错捕获try:fornumingen_func():print(num)exceptValueErrorase:print(f"捕获异常:{e}")

4. 生成器关闭(避免资源泄漏)

如果生成器中有未释放的资源(如文件句柄),可调用close()手动关闭:

defgen_func():f=open("test.txt","w")try:yield1yield2finally:f.close()# 关闭文件gen=gen_func()next(gen)gen.close()# 手动关闭,触发finally块

5. 滥用生成器(小数据场景没必要)

  • 小数据量(如1000个元素)用生成器反而增加函数调用开销,不如直接用列表;
  • 生成器的优势体现在大数据/无限序列/按需处理场景。
http://www.jsqmd.com/news/471796/

相关文章:

  • 个人笔记机器学习1
  • 实时手机检测-通用性能详解:4K图像单帧<80ms,支持30FPS视频流
  • MQTT 即时通讯实战:从 RabbitMQ 到 Spring Boot 全栈集成
  • 说说哈尔滨靠谱的纹眉纹绣机构,哪家性价比高? - myqiye
  • Qwen3-VL-4B Pro入门指南:图文问答、场景描述、OCR识别三合一
  • 网络安全工程师-作业5
  • 2026 智能咖啡机挑选方法,新手入门到进阶选购推荐指南 - 品牌2026
  • 告别原始命令操作运维,使用自然语言驱动运维 K8S集群、主机、网络设备相关操作
  • Docker镜像远程(离线)迁移教程
  • 震动传感器(STM32)
  • (一)基础:线性模型
  • Python爬虫实战:逆向解包 Unsplash 官方编辑精选合集!
  • 上海/北京高端腕表维修指南:江诗丹顿/欧米茄常见故障与科学养护解析 - 时光修表匠
  • React Hooks 设计思想与自定义 Hook 开发实践
  • V8引擎深度解密:Isolate隔离机制如何保障多环境安全执行
  • CSP与Nonce集成实战:Next.js、Nuxt、Remix官方方案详解
  • C语言完美演绎3-12
  • 2026年Shulex VOC优惠折扣码最新更新 | 功能详细拆解 - 麦麦唛
  • OpenClaw 第二篇:核心架构拆解——从一句指令到自动执行的全流程
  • API实战:CUDA实现数组求和—— 综合使用内存API、内核API、事件API,对比串行/并行性能
  • React Context API:状态管理与性能优化的探索
  • 2026连云港装修公司综合评分推荐:一份基于20+数据维度的权威报告 - GEO排行榜
  • 磁盘分区与文件系统
  • ArrayList动态扩容机制
  • 化繁为简:Access 与 SQL 创新指南(第一篇)
  • Vue 3 Composition API 的逻辑复用模式探索
  • 中国国家级地面气象站基本气象要素日值数据集(V3.0)
  • Netty源码分析---waken方法详解
  • Python爬虫实战:鸣枪起跑!深度抓取全国马拉松赛事报名情报!
  • Vue 响应式原理与依赖追踪机制解析