老王的厨房里挂着三张菜谱:红烧肉、糖醋鱼、麻婆豆腐。食客越来越多,老板跑来提要求:“每道菜出锅前,记一下花了多久,好算成本。”老王不想改菜谱——那是几十年练出来的手艺。这时帮厨小赵说:“王叔您照常做,我站旁边记就行。”于是,菜还是那道菜,出锅却多了一份时间记录。这位不修改菜谱、只负责在旁边加流程的帮厨,就是“装饰器”。 可生意再火,老王也发愁:自助餐要提前备 1000 份菜,冰箱(内存)塞得满满当当,剩下还浪费。他决定改成“按单炒菜”:客人点一道,他炒一道;炒到一半来了新单,他先记着当前进度,回头接着炒。这种“要一道给一道、随取随续”的接单模式,就是“生成器”。 这篇文章就用这间厨房,带你搞懂 Python 里两样高级武器:给函数配帮厨的装饰器,和按单炒菜的生成器。
学会装饰器与生成器,你就能看懂框架源码里那些“魔法”写法,写出高复用、省内存的生产级 Python 代码——这是从“会写”走向“会设计”的关键一步。
目录
可视化演示
通过文氏图展示条件概率P(A|B)=P(A∩B)/P(B)的计算过程,调节滑块改变事件概率,实时观察条件概率的变化。
本节配有交互动效(进入视口后自动加载,约 3KB)。
条件概率 P(A|B)=P(A∩B)/P(B) 后验概率公式 文氏图交集区域代表A与B同时发生 P(A|B) = P(A∩B) / P(B)
装饰器入门:给函数配个帮厨
老王的招牌菜“红烧肉”远近闻名。老板想记录每道菜的出菜时间,老王不想改菜谱——改坏了算谁的?帮厨小赵主动请缨:“菜谱不用动,我站在灶台边计时就行。出锅前我记一笔,菜还是您那盘菜。”装饰器就是这位小赵:不动原函数一行代码,在它的前后插入额外动作。
小赵能做到这一切,前提是 Python 里“函数也是对象”——函数可以像变量一样被传递、被赋值、被返回。既然老王(原函数)能被当作参数递给小赵,小赵就能包一层再还回来。在 Python 里,装饰器就是一个“接收函数、返回新函数”的普通函数。
最朴素的装饰器长这样:def timer(func): 定义一个包装函数 wrapper(*args, kwargs),在调用 func 前后做点手脚,最后返回 wrapper。而 @timer 写在函数定义上方,等价于一行赋值:func = timer(func)。@ 只是语法糖,把“帮我给函数加包装”这件事写得更直观。
有了帮厨队伍,老板的厨房开始规范起来:有的负责记录时间,有的负责检查摆盘。重要的是,哪道菜要配哪位帮厨,写在菜谱上方一行 @ 就够,菜谱本身一个字都不用改。装饰器之所以是“高级武器”,就是因为它把“横切关注点”(日志、计时、鉴权)从业务代码里剥离出来,让每道菜谱保持纯粹。
- original(原函数)老王的菜谱,也就是函数本体,它是被包装的对象。
- extra(附加动作)帮厨增加的记录、检查等额外步骤,它接收原函数产出的结果。
- 合成(∘,composition)装饰器把 original 送进 extra 的流程里,得到的新函数叫 decorated。调用它,等价于先跑原函数、再执行附加动作。
闭包原理:帮厨的备忘小抄
小赵记性好,但好记性不如烂笔头。主厨交代他“出锅前撒一把葱花”,他就把这条规矩写在随身小抄上。有一天小赵跳槽到隔壁餐厅,只要小抄还在,他给新主厨打下手时,依然会记得撒葱花。语法上,小抄上记的“葱花规矩”,并不属于新主厨的厨房——它属于小赵自己。
装饰器的内层函数(小赵)引用了外层函数(主厨)的局部变量(葱花规矩)。按理说,外层函数执行完就结束了,局部变量应该被回收。但 Python 发现内层函数还“惦记”着这个变量,就会把它打包进一个叫闭包的特殊对象里,跟着内层函数走。
技术细节有三点:第一,被“惦记”的变量叫自由变量;第二,它们存在内层函数的 __closure__ 元组里,每个单元对应一个变量;第三,如果内层函数要修改自由变量,必须显式声明 nonlocal,否则 Python 会认为它是一个新的局部变量。来看看一个标准闭包示例 make_adder(n):它接收一个数 n,返回一个新函数,新函数把任何输入都加上 n。
回到厨房:闭包就是那张小抄。装饰器能“记住”参数、记住配置,靠的全是这张小抄。很多框架里“携带配置的包装函数”,本质上都是闭包在发力——比如 flask 的路由注册、click 的命令行选项,都是先把参数包进闭包,再返回一个懂规矩的函数。
- make_adder(外层函数)主厨定规矩,接收参数 n,并返回一个内层函数作为“成品帮厨”。
- g(内层函数)小抄上写的是“加 n”,g 就是被返回的闭包函数,它把 n 与参数 x 相加。
- n(自由变量)n 不在 g 的参数列表里却被 g 引用,Python 特地把 n 装进闭包单元,让 n 跟着 g 去任何地方。
带参装饰器:能接单的帮厨工厂
老板又提新需求:“小赵,以后不光计时,还要在记录里写上菜名标签,方便对账。”小赵心想:光靠一张通用小抄不够了,得按老板的要求定制。于是出现了一种新角色:帮厨工厂。工厂根据订单参数——比如“标签前缀”——造出一个符合要求的帮厨。
对应到 Python,就是带参装饰器。它比普通装饰器多一层:最外层函数接收参数(如 repeat(3) 里的 3),返回一个真正的装饰器,这个装饰器再接收函数,返回包装函数。三层嵌套像套娃,但每一层职责清晰:最外层“接参数”,中间层“接函数”,最内层“接调用”。
经典例子 @repeat(3):repeat(3) 先执行,拿到参数 3,返回装饰器 decorator;decorator 作用于原函数,返回 wrapper;以后每次调用,原函数被执行 3 遍。整个过程可以拆解为“先定制、再包装”两步——和普通装饰器唯一的区别是,多了一道“先用参数定制”的工序。
帮厨工厂让老板的厨房彻底规范化:想给接口加上“限流 + 日志 + 超时”,写三个带参装饰器叠上去就行,每个帮厨由对应工厂定制、互不干扰。日常开发中,functools.lru_cache(maxsize=128)、flask 的 app.route('/path') 都是带参装饰器的典型代表——你传的是参数,拿到的是定制好的包装。
- repeat(3)(工厂接单)先调用最外层函数,参数 3 进入闭包,返回一个定制好的装饰器。
- decorator(f)(定制包装)装饰器接收原函数 f,返回包装函数 w,这一步和普通装饰器完全一致。
- w(最终交付)以后调用 w(x),内部会按工厂定的规矩,把 f 执行指定次数。
内置装饰器:厨房的三个固定工位
老王的厨房规模大了,设了三个固定工位。第一个是“独立帮工”:他只管按单做事,不需要知道主厨是谁,有点像外聘的计件师傅。第二个是“值班领班”:他代表整个厨房接活,知道厨房有哪些人、有什么资源。第三个是“扫码取餐口”:顾客扫个码,餐食就从窗口递出来,完全不用和后厨打交道。
这三个工位对应 Python 类的三个内置装饰器:@staticmethod(独立帮工:不接收 self 也不接收 cls,就是放在类里的普通函数)、@classmethod(值班领班:自动接收类 cls,能访问类属性、支持继承)、@property(扫码取餐口:把方法伪装成属性,obj.price 直接取值,背后其实在跑函数)。
关键区别在签名。实例方法写 def f(self, x);@staticmethod 转换后写成 def f(x)——self 消失了;@classmethod 则写成 def f(cls, x)——self 换成了 cls。@property 则把读操作变成 obj.attr,还可以搭配 setter 控制赋值。这些内置装饰器让类设计优雅许多:只读属性、工厂方法、工具函数都能各归其位。
三个工位让厨房既灵活又有秩序:外聘帮工只做事不问出身,值班领班代表集体做决策,扫码窗口把内部复杂度藏起来。内置装饰器不多,但用好它们,类的可读性立刻上一个台阶——这也是你读 Django、SQLAlchemy 源码时最常见的三种“类魔法”。
- Method(self, x)(实例方法)默认签名里有 self,代表实例自己,Python 调用时会自动传入。
- longmapsto(转型)
@staticmethod做了一次签名层面的“换人”,self 参数被移除。 - f(x)(普通函数)转型后不接收 self 也不接收 cls,像独立帮工,只做事、不问厨师是谁。
生成器基础:按单炒菜的大厨
自助餐模式让老王头疼:每天提前备 1000 份菜,冰箱塞爆,晚上剩下几百份倒掉。他决定改做“按单炒菜”:客人点一道,他炒一道;不点不炒,冰箱里永远只有一份食材。这就是生成器——按需生产,绝不囤货。
语法上,只要函数体里出现 yield 关键字,这个函数就“变身”了。注意:调用它不会执行函数体,而是返回一个生成器对象。next(gen) 或 for 循环才会真正驱动函数体运行,一路执行到 yield,暂停,把一个值“递”出来。这里有个极易踩的坑:写 gen = countdown(5) 时,厨房并没有开火,只是挂了单。
生成器的价值在内存。列表 list(range(108)) 会一次性吃掉几百 MB 内存;而生成器每次只产生一个数,内存恒定。它还能表达无限序列——比如不断吐出奇数,因为根本不需要事先存下来。代价是:它只能从前往后走一遍,不支持下标访问,也不能回头。这就像按单炒菜:菜出完就没了,想再吃要重新下单。
老王的冰箱终于空出来了。处理几 GB 日志、几百万条训练样本时,按单炒菜(生成器)让内存占用稳稳地停在“一盘菜”的量级——这是数据工程师和 AI 工程师处理海量数据的首选姿势。
- memory_list = O(n)列表一次性把 n 个元素全部装进内存,相当于冰箱里备齐 1000 份菜。
- memory_gen = O(1)生成器任意时刻只保留一个产出值,内存是常数,永不随数据量膨胀。
- 两者关系n 越大,差距越悬殊;生成器本质上是在用“时间换空间”。
生成器进阶:客人可以中途改单
老王正炒着宫保鸡丁,客人喊了一嗓子:“别放花生!”老王手一顿,把刚想倒进去的花生收回,调整完继续往下炒。这个“炒一半还能沟通”的能力,就是 send 方法——它能在生成器暂停时,从外部传一个值进去,代替 yield 表达式的结果。
gen.send(value) 把 value 送回暂停处的 yield 表达式,生成器接着往下跑,直到下一个 yield 再停下来。特别地,第一次唤醒不能用 send(非None),得用 gen.send(None) 或 next(gen)。有了 send,生成器就不只是“单向水管”,而是“双向对讲机”——这就是协程的雏形:两个函数互相喊话、交替推进。
同一个家族还有“生成器表达式”:(x * x for x in range(n))。它和列表推导式 [x * x for x in range(n)] 长得像,但前者是惰性的生成器,后者立即建出整个列表。写代码时一个括号之差,内存天壤之别。很多 AI 框架的数据加载器(DataLoader)就是靠生成器表达式 + send 类机制,实现“边训练边喂数据”。
客人能改单,主厨能沟通,厨房从“单向出菜”进化成“前后场对话”。很多异步框架的协程、async for 异步生成器,都是在这个机制上长出来的——理解了 send,你就在心里种下了一颗协程的种子。
- gen(暂停中的生成器)生成器运行到 yield 停住,像一个做到一半的厨师,等待外部消息。
- send(x_k)(客人喊话)x_k 被送回 yield 表达式,作为它的结果继续往下执行。
- 循环往复每 send 一次,生成器前进一段并再次暂停,形成双向对话般的交替推进。
yield from:整块甜品交给副厨
老王接了个婚宴大单,几十桌。他忙不过来,把甜品区整个交给副厨阿强:“阿强,甜品你全权负责,客人有什么特殊要求直接跟你对接,不用经过我。”这种“整块移交 + 直接对接”的委托方式,就是 yield from。
作用:在一个生成器里写 yield from sub_gen,等于把 sub_gen 整个“外包”出去,主生成器不再逐个搬运 sub_gen 的值,而是让调用方直接和 sub_gen 打交道。写出来最省,读起来最清楚——你看代码时一眼就知道“这一整段是委托出去的”。
简单场景下,yield from s 和 for v in s: yield v 结果一样;但前者会自动透传 send()、throw()、close() 等消息,还能拿到子生成器的返回值。协程嵌套协作时,这种“协议透传”是普通 for 循环做不到的——就像老王如果一道道转达客人的要求,不仅慢,还可能传错话。
老王松了口气:每块业务交给一个副厨,主厨房只管协调。代码也一样——生成器层层委托,结构像菜单一样清晰,消息像服务员一样直达对应后厨。Python 标准库 contextlib、各种协程框架里,yield from 都是不可或缺的“委托枢纽”。
- yield from s(委托)把子生成器 s 整体外包出去,主生成器只做传话人。
- for+yield(等价写法)手动逐个转发值时,二者行为一致,这是多数入门教程展示的样子。
- Longleftrightarrow(关键差异)等价只在“只取值”时成立,send/throw 的协议透传是 yield from 独有能力。
实战应用:厨房的黄金时代
厨房转型完成:每个厨师都配了帮厨(日志装饰器),招牌菜提前卤好冷藏、客人点了热一下再上(缓存装饰器),大订单按单炒菜绝不囤货(生成器流式处理)。这套组合拳让餐厅既快又省——装饰器管“加能力”,生成器管“省资源”。
最常见的实战:日志装饰器——给每个 API 端点记录调用时间、入参、出参,异常时自动告警,业务函数保持干净;缓存装饰器——functools.lru_cache 记住函数结果,相同参数直接秒回,省去重复计算;流式读文件——for line in open(...) 逐行读取,几十 GB 日志也只占一行内存,配合生成器管道可以边读边统计。
还有装饰器叠加:@a 在 @b 之上时,@b 先作用(离函数近的先包),@a 再包一层。这就像帮厨排队:最里面的帮厨最先接触原函数,最外面的帮厨最先接触调用方。叠加顺序不同,行为就不同——比如“先鉴权再计时”和“先计时再鉴权”,失败的请求计不计时,结果完全不一样。
从一张菜谱到一整套厨房体系,装饰器和生成器从来不是孤立技巧:装饰器让横切逻辑复用,生成器让数据流受控。框架作者每天都在用这套思维方式写代码——你掌握了它们,读源码时那些“魔法”就会变成“原来如此”。
- t_func(原函数耗时)老王炒菜本身的时间,不掺任何水分的业务逻辑耗时。
- t_wrapper(包装开销)帮厨计时、记录日志、查缓存等附加动作的时间。
- t_total(总耗时)调用方感受到的总时间,约等于两者相加;缓存命中时 t_func 趋近于 0。
AI 应用场景
AI 服务统一鉴权与观测
阶段:模型部署与监控(MLOps)
解决的问题:AI 服务往往有几十个推理接口,传统做法是每个接口单独写鉴权、耗时统计、Token 计数,重复代码多、容易漏。用装饰器把“鉴权 + 计时 + 日志”写成通用包装,所有接口一行 @ 搞定,新模型上线零成本接入。
具体案例:某智能客服团队给 12 个模型端点统一加装饰器后,新模型上线改造成本从 1 天降到 10 分钟;线上出问题时,通过装饰器统一打印的 trace 日志,10 秒定位慢请求。
训练数据的流式加载
阶段:数据处理与训练管线
解决的问题:图像数据集动辄几十 GB,一次性读入内存直接 OOM。用生成器按 batch 产出数据,在每次 yield 前做随机裁剪、翻转等增强,内存恒定在一个 batch 的量级,训练循环代码也变得极其简洁。
具体案例:某 CV 团队训练目标检测模型时,用生成器实现“边读图、边增强、边喂 GPU”,4 卡训练 8 小时内存稳定,数据加载不再是训练瓶颈。
推理日志实时异常检测
阶段:模型监控与告警
解决的问题:推理服务每天产生几十 GB 日志,全量 grep 太慢。用生成器逐行流式解析,遇到错误率或延迟飙升立刻触发告警,内存占用恒定,管道式代码也容易扩展新指标。
具体案例:在线推荐系统用生成器管道串联“读日志 → 解析 → 聚合指标 → 告警”,异常检测延迟从分钟级降到秒级,磁盘 IO 压力也大幅下降。
Python 演示
import timefrom functools import wraps def timer(func): """装饰器: 给原函数附加耗时统计, 原函数本身一字不改""" @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) cost = time.perf_counter() - start print("{} 运行耗时 {:.4f} 秒".format(func.__name__, cost)) return result return wrapper @timerdef sum_to_n(n): """普通函数: 一次性算出 0 到 n 的和""" return sum(range(n + 1)) def countdown(n): """生成器函数: 执行到 yield 暂停并吐出一个值, 下次 next 再续上""" while n >= 0: yield n n -= 1 def read_lines(path): """生成器: 逐行读取大文件, 内存永远只占一行""" with open(path, "r", encoding="utf-8") as f: for line in f: yield line.rstrip("\n") if __name__ == "__main__": total = sum_to_n(1000000) print("sum_to_n(1000000) =", total) print("countdown(5) 逐个产出:") for x in countdown(5): print(" 得到", x) gen = read_lines("no_such_file.txt") print("创建 read_lines 生成器后函数体未执行, 文件尚未打开") print("生成器类型:", type(gen).__name__)
def wrapper(*args, **kwargs):万能传菜口,不管原函数收什么参数都能接住并转发。@wraps(func)把原函数的名字、文档字符串等元信息复制到包装函数上,调试时不会看到满屏的 "wrapper"。@timer语法糖,等价于sum_to_n = timer(sum_to_n),把原函数名重新绑定到包装函数。yield n暂停函数执行并把 n 递出去,下次next()从这一行继续。while n >= 0:配合 yield 形成“每取一次、状态推进一次”的生成循环。for line in f:文件对象本身可迭代,逐行读取正是生成器流式处理的经典写法。gen = read_lines("no_such_file.txt")创建生成器不执行函数体,所以文件根本没被打开,完美展示惰性求值。for x in countdown(5):for 循环本质是不断调用next(),直到StopIteration为止。
常见误区
误区我以为 `@timer` 之后,原函数就被替换销毁了,以后想用原函数,只能重新写一遍或者提前保存一份拷贝。
正解装饰器只是把函数名重新绑定到包装函数上,原函数对象一直存在,配合 functools.wraps 还能通过包装函数的 __wrapped__ 属性找回来。这就像老王配了帮厨小赵后,老王本人还在后厨炒菜,只是“老王”这个名字现在默认指向“带帮厨的完整流程”。想绕过帮厨,找到原函数对象直接调用即可,但日常调用走的是新流程。
为何常见:因为外部调用 func() 时,行为已经完全变成包装后的样子,很难意识到底层函数对象仍然活着。
误区我写了一个带 yield 的函数,然后 `g = mygen(5)`,以为这时候函数已经跑起来、停在第一个 yield,可以直接拿 g 当结果用。
正解调用生成器函数只会返回一个生成器对象,函数体一行代码都没执行。要等调用 next(g) 或用 for 迭代,函数体才开始跑,并在遇到 yield 时暂停。就像给厨师下单后,厨房并没有立刻开火,只有后厨喊“下一道!”才动锅——这也是“惰性”的由来。
为何常见:普通函数“调用即执行”的习惯太根深蒂固,忽略了带 yield 的函数被“工厂化”的差异。
误区惰性求值既省内存又省时间,以后凡是处理大批量数据,无脑用生成器就对了,性能肯定吊打列表。
正解生成器只保证内存占用低(O(1) 级别),并不保证速度快。逐项生成有函数帧切换和迭代协议的开销,在很多场景下比一次性构建列表慢。比如小数据量循环,列表反而更快。真正优势是“内存受控”和“流式处理”,适合海量数据和无界序列。自助餐虽浪费食材,但客人多时出餐快;按单炒菜省食材,但每道都现炒。
为何常见:“懒”听起来就像“快”,把省内存误当省时间,是惰性求值最常见的认知偏差。
误区反正都是放在类里、不用实例也能调用的装饰器,我就随便混着用,反正都能跑。
正解@staticmethod 装饰的函数不接收 self 也不接收 cls,就是放在类命名空间里的普通函数;@classmethod 装饰的函数会接收 cls(类本身),可以访问类属性、调用其他类方法,并且继承时正确传入子类。这就像“独立帮工”(不关心主厨是谁)和“值班领班”(代表整个厨房)的区别。混用会导致取不到类信息、继承行为出错。
为何常见:在简单类里两者调用方式看起来一样,只有用到继承和类属性时才暴露差异。
误区我看了很多例子,`yield from sub` 不就是 `for v in sub: yield v` 的缩写嘛,学会了缩写就完事了。
正解在“只取值”的简单场景两者等价;但 yield from 会把 send()、throw()、close() 这些消息自动透传给子生成器,并处理子生成器的返回值——这是普通 for+yield 做不到的。就像主厨把甜品整块交给副厨,并告诉服务员“甜品的所有要求直接对接副厨”,而不是一道道帮你转达。
为何常见:入门资料大多只演示简单迭代,很少展示 send/throw 的透传差异,容易形成“纯缩写”的印象。
面试题
Q1. 在 Python 中,@timer 放在函数定义上方,等价于下列哪种写法?
- A.
func(timer)直接调用 - B.
func = timer(func)← 正确答案 - C.
timer = func(timer) - D.
timer(func())自动调用
答案:B。@timer 是语法糖,等价于把函数名重新绑定到包装结果:func = timer(func)。选项 B 正确;A 把函数当参数传给 timer 的顺序反了,C 把赋值方向弄反,D 是直接调用函数拿返回值,都不对。就像帮厨小赵站在老王身边,“老王”这个名字仍然存在,但喊一声过来的是“配了帮厨的老王”。
Q2. 定义一个含有 yield 语句的函数并执行 gen = countdown(10),此时发生了什么?
- A.立即计算出全部十个结果
- B.创建生成器但函数体未执行← 正确答案
- C.函数体执行到第一个暂停状态
- D.返回一个只读的元组对象
答案:B。函数体里出现 yield,调用它只返回生成器对象,函数体一行都不跑。只有 next() 或 for 循环才会驱动执行。生成器函数像挂单厨师:你下单(创建对象),厨房不开火,直到有人喊“下一道”才动锅。A 错在以为立即算完,C 错在以为已执行,D 错在返回类型是生成器而非元组。
Q3. functools.wraps(func) 的主要作用是什么?
- A.让包装函数继承原函数的元信息← 正确答案
- B.让包装函数运行速度更快
- C.让原函数可以接受更多参数
- D.让原函数保持原有的返回值
答案:A。wraps 把原函数的名字、__doc__ 等元信息复制到包装函数上,否则调试时看到的函数名全是 "wrapper"。这是帮厨把小抄上先誊一份主厨的签名,方便外人认出这道菜出自谁手。B 的性能、C 的参数数目、D 的返回值都与 wraps 无关,那是装饰器本身的逻辑决定的。
Q4. 装饰器函数内部通常需要定义并返回一个内层函数,主要原因是?
- A.内层函数才能访问原函数参数
- B.必须要有闭包才能保存变量
- C.包装逻辑需要延迟到调用时执行← 正确答案
- D.内层函数速度比外层函数更快
答案:C。装饰器在函数定义时执行一次,而真正的包装逻辑要等函数被调用才跑,所以必须用内层函数把逻辑包起来返回。若装饰器直接执行逻辑,那在导入模块时就跑完了。A、B 是闭包附带的好处但非根本原因,D 毫无依据。像小赵不能在主厨上班前就替他切好菜,要等客人点菜时再动手。
Q5. 关于生成器与列表的区别,下列说法正确的有哪些?
- A.生成器按需产出并节省内存← 正确答案
- B.生成器可以重复迭代任意多次
- C.生成器对象只能顺序访问← 正确答案
- D.生成器一定比列表运行更快
答案:AC。生成器按需产出,内存只占当前一项,且只能迭代一遍、只能顺序访问(不能下标),所以 A、C 正确。B 错:生成器是一次性消耗品,想重来必须重新调用函数创建新对象。D 错:生成器省内存但不一定省时间,逐项产出有额外开销。自助餐 vs 按单炒菜:按单炒菜省料,但每道都是现炒,不保证更快。
Q6. 闭包中内层函数使用的“自由变量”存储在哪里?
- A.全局命名空间的变量表里
- B.外层函数的名字空间之外
- C.内层函数代码对象的字节码里
- D.闭包单元里由函数对象携带← 正确答案
答案:D。自由变量存在闭包单元(__closure__)里,挂在返回的函数对象上,随函数一起传递。A 错:它不是全局变量;B 错:它恰恰被内层函数捕获;C 错:字节码是代码指令,不是变量存储处。帮厨的小抄存放在帮厨的口袋里(函数对象的属性),不在主厨的厨房里,也不在餐厅大堂的公告栏上。
Q7. 下列哪个操作能让一个已耗尽的生成器重新从头开始迭代?
- A.next(gen) 调用两次即可
- B.传入 list(gen) 再取出来
- C.gen.send(None) 恢复运行
- D.重新调用生成器函数创建新对象← 正确答案
答案:D。生成器是一次性对象,迭代完就耗尽,无法复位。想重头再来,只能重新调用生成器函数,创建一个全新对象。A 会继续向下走直到 StopIteration;B 会把剩余元素全部消费掉;C 只是恢复已暂停的生成器,对已耗尽的无效。厨房按单炒菜,炒完这轮就没存货,想再吃一遍得重新下单。
Q8. yield from sub_gen 与 for v in sub_gen: yield v 的本质区别是什么?
- A.前者迭代速度必然大幅提升
- B.前者自动透传 send 与 throw← 正确答案
- C.前者会复制一份新的子生成器
- D.前者只能处理可迭代的生成器
答案:B。在“只取值”的简单场景两者等价,但 yield from 会把 send()、throw()、close() 等协议消息自动透传给子生成器,还能拿到子生成器的返回值,这是 for+yield 做不到的。A 无依据;C 不会复制;D 说反了——yield from 后面跟任意可迭代对象都可以。像主厨把甜品区全权交给副厨,客人要求直接对接副厨,而不是一道道转达。
Q9. 下列哪些属于 Python 内置的装饰器?
- A.@staticmethod← 正确答案
- B.@classmethod← 正确答案
- C.@property← 正确答案
- D.@synchronized
答案:ABC。@staticmethod、@classmethod、@property 都是 Python 内置装饰器,对应三种类方法形态;@synchronized 是 Java 风格关键字,Python 没有。staticmethod 不传 self,classmethod 传 cls,property 把方法变属性访问。它们就是厨房的三个固定工位:独立帮工、值班领班、扫码取餐口。
Q10. 关于生成器协程(send 与暂停恢复),下列说法正确的有哪些?
- A.send 能把数值回传给暂停处← 正确答案
- B.首次唤醒要用 send(None)← 正确答案
- C.生成器只能被 for 循环消费
- D.生成器可被多个线程并发消费
答案:AB。send 能把数值回传给暂停处的 yield 表达式,A 正确;生成器首次唤醒不能 send 非 None 值,必须用 send(None) 或 next(gen),B 正确。C 错:next(gen) 同样可以逐个取值,for 只是更方便的语法。D 错:生成器不是线程安全的,多线程并发消费会乱序。协程雏形正是靠“send 传值 + 暂停恢复”这对组合实现的。