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

Python 面试高频:装饰器、迭代器、生成器和上下文管理器一次讲清

Python 面试高频:装饰器、迭代器、生成器和上下文管理器一次讲清

开场:这些题为什么总被问?

如果面试官问 Python 基础,除了昨天讲过的对象、引用、可变默认参数、浅拷贝、GIL,后面大概率会追这几个问题:

  • 装饰器本质是什么?
  • 闭包为什么能记住外层变量?
  • functools.wraps有什么用?
  • 迭代器和可迭代对象有什么区别?
  • 生成器为什么省内存?
  • yield到底暂停了什么?
  • with语句背后发生了什么?
  • 上下文管理器为什么适合管理文件、连接和锁?

这些问题不是为了考你会不会背术语,而是看你有没有理解 Python 的几个核心设计:

函数可以像对象一样传递,遍历依赖统一协议,惰性计算可以保存执行现场,资源释放应该交给明确的进入和退出边界。

这句话听起来长,拆开就是今天这篇文章的四条线:

  1. 装饰器和闭包:函数也是对象,函数可以包函数。
  2. 迭代器协议:for循环背后不是魔法,而是iter()next()
  3. 生成器:yield让函数可以暂停、恢复、逐个产出数据。
  4. 上下文管理器:with让资源进入和退出有稳定边界。

这几个点学完,再去看 FastAPI 的路由装饰器、依赖注入、流式响应和数据库连接生命周期,会顺很多。

一、装饰器:本质是“函数包一层函数”

先看最常见的写法:

@timerdefquery_user(user_id):return{"id":user_id}

很多人第一次看到@timer会把它当成一种特殊语法。其实装饰器最核心的理解很简单:

装饰器就是一个接收函数、返回新函数的函数。

上面的代码基本等价于:

defquery_user(user_id):return{"id":user_id}query_user=timer(query_user)

也就是说,@timer不是在函数调用时才执行。它会在函数定义完成后,把原函数传给timer,再把返回结果重新绑定给原函数名。

写一个最小装饰器:

deftimer(func):defwrapper(*args,**kwargs):print("before")result=func(*args,**kwargs)print("after")returnresultreturnwrapper@timerdefadd(a,b):returna+bprint(add(1,2))

执行流程可以这样理解:

定义 add -> 把 add 传给 timer -> timer 返回 wrapper -> add 这个名字重新绑定到 wrapper -> 调用 add(1, 2) 时,实际调用 wrapper(1, 2) -> wrapper 内部再调用原始 func(1, 2)

这里有两个关键点:

第一,函数在 Python 里是对象。

函数可以赋值给变量:

defhello():return"hello"f=helloprint(f())

函数也可以作为参数传给另一个函数:

defrun(fn):returnfn()print(run(hello))

函数还可以作为返回值:

defouter():definner():return"inner"returninner fn=outer()print(fn())

装饰器就是把这三个能力组合起来:函数传进去,新函数返回来。

第二,wrapper(*args, **kwargs)是为了尽量兼容原函数参数。

如果原函数有不同参数:

defadd(a,b):...defget_user(user_id,verbose=False):...defsave(**payload):...

装饰器不应该把参数写死。*args接收位置参数,**kwargs接收关键字参数,这样包装函数才能转发不同形状的调用。

面试回答模板

如果面试官问“装饰器本质是什么”,可以这样答:

装饰器本质上是一个接收函数并返回函数的可调用对象。@decorator只是语法糖,等价于func = decorator(func)。它通常通过内部的wrapper函数在调用原函数前后加入日志、鉴权、计时、缓存等逻辑。

二、闭包:为什么 wrapper 还能拿到原函数?

上面的timer里有一个容易被忽略的问题:

deftimer(func):defwrapper(*args,**kwargs):result=func(*args,**kwargs)returnresultreturnwrapper

timer已经执行结束了,为什么wrapper后面还能用func

答案就是闭包。

闭包是指内部函数引用了外部函数作用域里的变量,并且这个内部函数被返回或传递到外部继续使用。

看一个更小的例子:

defmake_counter():count=0definc():nonlocalcount count+=1returncountreturninc counter=make_counter()print(counter())# 1print(counter())# 2print(counter())# 3

make_counter()执行结束后,局部变量count并没有马上消失,因为返回的inc函数还引用着它。

这就是闭包的作用:

外部函数变量 -> 被内部函数引用 -> 内部函数返回到外部 -> 变量随内部函数一起保留下来

装饰器里的wrapper能继续拿到func,也是这个原因。

nonlocal是干什么的?

如果内部函数只是读取外层变量,通常不用写nonlocal

defouter():name="python"definner():returnnamereturninner

但如果内部函数要重新赋值,就要写nonlocal

defmake_counter():count=0definc():nonlocalcount count+=1returncountreturninc

不写nonlocal,Python 会把count += 1里的count当成内部函数的局部变量处理,结果就会报错。

带参数的装饰器,其实多包了一层

普通装饰器:

@timerdefadd(a,b):returna+b

等价于:

add=timer(add)

带参数的装饰器:

@retry(times=3)defrequest_api():pass

等价于:

request_api=retry(times=3)(request_api)

所以带参数装饰器通常是三层函数:

defretry(times):defdecorator(func):defwrapper(*args,**kwargs):last_error=Nonefor_inrange(times):try:returnfunc(*args,**kwargs)exceptExceptionasexc:last_error=excraiselast_errorreturnwrapperreturndecorator

三层分别负责:

层级作用
retry(times)接收装饰器参数
decorator(func)接收被装饰的原函数
wrapper(*args, **kwargs)接收原函数调用参数

这个结构一旦看懂,很多 Python 框架里的@xxx(...)就不会显得神秘了。

闭包常见坑:状态会被保留下来

闭包能保留状态,这是优点,也可能是坑。

比如下面这个装饰器记录调用次数:

defcount_calls(func):count=0defwrapper(*args,**kwargs):nonlocalcount count+=1print(f"call count ={count}")returnfunc(*args,**kwargs)returnwrapper

每个被装饰的函数都会有自己的count。如果你把这个状态用于缓存、鉴权、限流,就要想清楚状态的作用域:是每个函数独立,还是全局共享,是否线程安全,是否会在长生命周期进程里越积越多。

面试回答模板

如果面试官问“闭包是什么”,可以这样答:

闭包是内部函数引用了外部函数作用域中的变量,并且内部函数在外部函数结束后仍然继续存在。Python 会把被引用的外层变量随内部函数一起保存下来。装饰器里的wrapper能记住原函数func,就是典型闭包。

三、为什么装饰器要加functools.wraps

上面的装饰器还能跑,但有一个问题:

deftimer(func):defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapper@timerdefadd(a,b):"""Add two numbers."""returna+bprint(add.__name__)print(add.__doc__)

你可能会得到:

wrapper None

因为add这个名字已经被重新绑定到wrapper了。此时外部看到的函数元信息,不再是原来的add,而是包装后的wrapper

这在普通脚本里可能没什么问题,但在框架里就可能出事:

  • 日志里函数名全是wrapper,排查困难。
  • 文档工具拿不到原函数说明。
  • 测试和调试信息变得不直观。
  • 某些依赖函数签名或元信息的框架会受到影响。

所以正式写装饰器时,通常要加functools.wraps

fromfunctoolsimportwrapsdeftimer(func):@wraps(func)defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapper

wraps会把原函数的一些元信息复制到wrapper上,比如__name____doc____module____annotations__等。

这不是为了“好看”,而是为了让包装后的函数尽量像原函数。

面试回答模板

装饰器会用wrapper替换原函数,如果不加functools.wraps,外部看到的函数名、文档、注解等元信息可能变成wrapperwraps的作用是把原函数的元信息复制到包装函数上,方便调试、文档生成和框架反射。

四、迭代器:for循环背后不是魔法

接着看第二组高频题:迭代器和可迭代对象。

先看平时最常见的写法:

foritemin[1,2,3]:print(item)

你可以把它理解成:

items=iter([1,2,3])whileTrue:try:item=next(items)exceptStopIteration:breakprint(item)

也就是说,for循环背后主要依赖两个动作:

动作含义
iter(obj)拿到一个迭代器
next(iterator)从迭代器里取下一个值

当没有更多数据时,迭代器会抛出StopIterationfor循环捕获它并结束循环。

可迭代对象和迭代器有什么区别?

一句话:

可迭代对象是“能被iter()转成迭代器的对象”;迭代器是“能被next()不断取值的对象”。

常见可迭代对象:

  • list
  • tuple
  • dict
  • set
  • str
  • 文件对象
  • 生成器对象

迭代器对象要实现两个方法:

classCountDown:def__init__(self,start):self.current=startdef__iter__(self):returnselfdef__next__(self):ifself.current<=0:raiseStopIteration value=self.current self.current-=1returnvalueforninCountDown(3):print(n)

输出:

3 2 1

这里CountDown同时是可迭代对象,也是迭代器,因为它的__iter__()返回了自己。

但列表不是迭代器:

nums=[1,2,3]it=iter(nums)print(next(it))# 1print(next(it))# 2

nums是可迭代对象,it才是迭代器。

这个区别在面试里很容易被问:

list可以for循环,但它本身不是迭代器。它是可迭代对象,调用iter(list)后才会得到迭代器。

迭代器是有状态的

迭代器会记住自己遍历到哪里了:

nums=[1,2,3]it=iter(nums)print(next(it))# 1print(next(it))# 2forxinit:print(x)# 只会输出 3

这意味着迭代器通常是一次性消费的。你把一个迭代器传给两个地方一起用,很容易出现“怎么少数据了”的问题。

面试回答模板

可迭代对象是实现了可迭代协议、能被iter()获取迭代器的对象;迭代器是实现了__next__()的对象,每次next()返回一个值,没有值时抛出StopIterationfor循环本质上就是先调用iter(),再不断调用next()

五、生成器:yield让函数暂停和恢复

生成器是 Python 面试里非常高频的点。

先看普通函数:

defbuild_list():result=[]foriinrange(3):result.append(i)returnresultprint(build_list())# [0, 1, 2]

普通函数一调用,就会一路执行到return,然后结束。

再看生成器函数:

defgen_numbers():foriinrange(3):yieldi g=gen_numbers()print(next(g))# 0print(next(g))# 1print(next(g))# 2

只要函数体里出现yield,调用这个函数时不会立刻执行函数体,而是返回一个生成器对象。

真正执行发生在你调用next(g)的时候。

执行到yield i时,函数会:

  1. 产出当前值i
  2. 暂停在这一行。
  3. 保存当前局部变量和执行位置。
  4. 等下一次next()再从暂停处继续。

这就是生成器和普通函数最大的区别:

普通函数:一次执行到 return 生成器函数:多次 next,多次暂停和恢复

生成器为什么省内存?

假设你要处理 1000 万行数据。

列表写法:

defload_all_rows(path):rows=[]withopen(path,"r",encoding="utf-8")asf:forlineinf:rows.append(line.strip())returnrows

这个函数会把所有数据都放进内存。

生成器写法:

defread_rows(path):withopen(path,"r",encoding="utf-8")asf:forlineinf:yieldline.strip()

这个函数每次只产出一行。调用方需要一行,就取一行。

forrowinread_rows("data.txt"):handle(row)

所以生成器省内存的原因不是它“压缩了数据”,而是:

它不一次性构造完整结果,而是按需逐个产出。

生成器也是迭代器

生成器对象可以直接用于for循环:

defgen():yield1yield2forxingen():print(x)

因为生成器对象本身就是迭代器。它有__iter__(),也有__next__()

生成器常见坑:只能消费一次

g=(x*2forxinrange(3))print(list(g))# [0, 2, 4]print(list(g))# []

第二次为什么是空?

因为第一次list(g)已经把生成器消费完了。生成器记住自己的执行位置,走到终点后就结束了。

如果你需要重复遍历,就要重新创建生成器:

defmake_gen():return(x*2forxinrange(3))print(list(make_gen()))print(list(make_gen()))

yieldreturn有什么关系?

生成器函数里可以写return,但它不是像普通函数那样返回最终列表。

defgen():yield1returnyield2

return会结束生成器。外部继续next()时会触发StopIteration

日常面试里不需要把生成器协议讲得特别深,但要能说清楚:

  • yield产出值并暂停。
  • 下一次next()从暂停位置继续。
  • 生成器按需产出,所以适合大数据流、文件读取、分页拉取、流式响应。
  • 生成器是一次性消费的。

面试回答模板

生成器函数是包含yield的函数,调用它不会立即执行函数体,而是返回生成器对象。每次next()会执行到下一个yield,产出值后暂停,并保存当前执行状态。生成器按需产出数据,不需要一次性把所有结果放进内存,所以适合处理大文件、数据流和惰性计算。

六、上下文管理器:with解决的是资源退出边界

最后看with

最常见写法:

withopen("data.txt","r",encoding="utf-8")asf:content=f.read()

它解决的问题很朴素:

无论代码正常结束,还是中途抛异常,都要把文件关掉。

不用with,你可能会写:

f=open("data.txt","r",encoding="utf-8")try:content=f.read()finally:f.close()

with就是把这种try/finally资源管理模式变成了协议。

with背后的两个方法

一个对象只要实现了上下文管理协议,就可以放进with

classManagedResource:def__enter__(self):print("enter")returnselfdef__exit__(self,exc_type,exc,traceback):print("exit")returnFalsewithManagedResource()asresource:print("use resource")

执行顺序:

调用 __enter__() -> 把返回值绑定给 as 后面的变量 -> 执行 with 代码块 -> 调用 __exit__()

即使代码块中抛异常,__exit__()也会被调用。

__exit__()的三个参数用于接收异常信息:

参数含义
exc_type异常类型
exc异常对象
traceback异常调用栈

如果__exit__()返回True,表示异常被处理,不再向外抛;返回FalseNone,异常会继续向外传播。

日常写资源管理时,大多数情况下应该让异常继续抛出去,不要随便吞异常。

contextlib.contextmanager简化写法

如果不想专门写一个类,可以用contextlib.contextmanager

fromcontextlibimportcontextmanager@contextmanagerdefmanaged_resource():print("enter")try:yield"resource"finally:print("exit")withmanaged_resource()asresource:print(resource)

这个写法里:

  • yield前面的代码相当于__enter__()
  • yield产出的值会绑定给as resource
  • finally里的代码相当于__exit__(),负责释放资源。

这也解释了为什么今天把生成器和上下文管理器放在一起讲:contextmanager装饰器就是用生成器来写上下文管理器。

上下文管理器适合哪些场景?

常见场景:

  • 文件打开和关闭。
  • 数据库连接获取和释放。
  • 事务提交和回滚。
  • 锁的获取和释放。
  • 临时切换配置,再恢复旧配置。
  • 测试里临时 mock 某个资源。

它的价值不是少写几行代码,而是把资源生命周期写成明确边界:

进入 with:拿资源 代码块中:使用资源 离开 with:释放资源

这比依赖对象析构、垃圾回收或“记得手动 close”稳定得多。

面试回答模板

上下文管理器用于管理资源的进入和退出。with语句会先调用对象的__enter__(),代码块结束后调用__exit__(),即使中间抛异常也会执行退出逻辑。它适合管理文件、连接、锁和事务,本质上是把try/finally资源释放模式协议化。

七、把这几个点串起来

今天这几个知识点看起来分散,其实可以串成一条线:

知识点一句话理解常见用途
装饰器函数包一层函数日志、鉴权、缓存、重试、路由注册
闭包内部函数记住外层变量装饰器参数、状态保存、函数工厂
wraps保留原函数元信息调试、文档、框架反射
迭代器按协议逐个取值for循环、自定义遍历
生成器暂停和恢复的惰性迭代器大文件、数据流、分页、流式返回
上下文管理器明确资源进入和退出文件、连接、事务、锁

后面学 FastAPI 时,这些点会反复出现:

  • @app.get(...)是装饰器。
  • 自定义鉴权、日志、限流,也常用装饰器或中间件。
  • 依赖函数、路由函数的签名和元信息很重要,所以装饰器不能乱丢函数信息。
  • 流式响应常常和生成器、异步生成器有关。
  • 数据库 Session、事务、文件资源,都绕不开上下文管理。

所以今天这篇不是为了堆语法,而是先把 Python 框架背后的语言机制补齐。

八、面试快速复盘

最后给一份可直接复述的回答清单。

1. 装饰器是什么?

装饰器是接收函数并返回新函数的可调用对象,@decorator等价于func = decorator(func)。它常用于在不修改原函数代码的情况下增加日志、计时、鉴权、缓存、重试等逻辑。

2. 闭包是什么?

闭包是内部函数引用外部函数作用域里的变量,并且内部函数在外部函数结束后仍然存在。Python 会把这些被引用的变量和内部函数一起保存下来。

3. 为什么装饰器要用functools.wraps

因为装饰器会用wrapper替换原函数。如果不加wraps,函数名、文档、注解等元信息可能丢失,影响日志、调试、文档生成和框架反射。

4. 可迭代对象和迭代器有什么区别?

可迭代对象能被iter()获取迭代器;迭代器能被next()不断取值,没有数据时抛出StopIterationfor循环本质上就是iter()next()

5. 生成器为什么省内存?

生成器不会一次性构造完整结果,而是每次执行到yield时产出一个值并暂停。下一次next()再从暂停位置继续,所以适合大文件、数据流和惰性计算。

6.with语句背后是什么?

with背后是上下文管理协议。进入代码块前调用__enter__(),离开代码块时调用__exit__(),即使中间抛异常也会执行退出逻辑,适合管理文件、连接、事务和锁。

总结

Python 基础面试里,装饰器、迭代器、生成器和上下文管理器不是孤立语法点。

它们共同指向一件事:

Python 很多“简洁写法”的背后,都是对象、协议和执行状态管理。

装饰器靠函数对象和闭包扩展行为;迭代器靠协议统一遍历;生成器靠暂停和恢复做惰性计算;上下文管理器靠进入和退出边界管理资源。

把这些机制搞懂,再去看 FastAPI、Django、SQLAlchemy、pytest 这些框架时,很多“框架魔法”都会变成可以解释的普通 Python 机制。

参考资料

  • Python 官方文档:Data model - Objects, values and types:https://docs.python.org/3/reference/datamodel.html
  • Python 官方文档:Compound statements - Function definitions and decorators:https://docs.python.org/3/reference/compound_stmts.html#function-definitions
  • Python 官方文档:Iterator types:https://docs.python.org/3/library/stdtypes.html#iterator-types
  • Python 官方文档:Yield expressions:https://docs.python.org/3/reference/expressions.html#yield-expressions
  • Python 官方文档:With statement:https://docs.python.org/3/reference/compound_stmts.html#the-with-statement
  • Python 官方文档:contextlib:https://docs.python.org/3/library/contextlib.html
  • Python 官方文档:functools.wraps:https://docs.python.org/3/library/functools.html#functools.wraps
http://www.jsqmd.com/news/959996/

相关文章:

  • 告别Excel和Word!用IBM DOORS管理需求,这5个功能让我效率翻倍
  • 【运维】Linux定时任务 定时执行脚本
  • Python函数:递归函数的定义与阶乘案例实现
  • 保姆级教程:用MQTT.fx的JS脚本5分钟模拟智能家居设备联动
  • 因果决策+分位数回归:让补货决策真正量化风险边界
  • LIO-SAM建图总跑飞?别急着调参,先检查IMU内参标定(附imu_utils保姆级教程)
  • Serverless超限怎么办?用混合架构为重载请求开辟专用通路
  • 新手福音:用快马AI将文字描述转为ER图,轻松入门数据库设计
  • Streamlit数据应用开发:Python脚本一键生成交互式Web看板
  • 别再只用plt.show()了!聊聊IPython里fig.show()的正确打开方式(附Matplotlib版本适配指南)
  • 【运维】Linux 磁盘分区相关 挂载分区卸载分区等
  • 从 MySQL 迁移到阿里云 AnalyticDB MySQL:零改造百倍加速实战教程
  • AI时代个人效能操作系统:教育设计×自由职业×注意力管理
  • 新手入门指南:基于快马AI生成你的第一个贴吧内容展示网页
  • 张量列车分解与低秩插值技术解析
  • 深度解析:XposedRimetHelper如何通过Hook技术实现智能虚拟定位
  • 别再搞混了!用Python手把手教你计算大气遥感中的散射角(附6S模型验证代码)
  • 被动调Q激光器MATLAB仿真工具:速率方程建模+脉冲参数自动提取(含Nd:YAG/Yb光纤示例)
  • 排查PCIe设备不识别?从硬件角度理解Receiver Detect失败的可能原因
  • 别再手动改路径了!PyQt5中pyrcc5.exe的3种高效定位方法(附Anaconda虚拟环境实战)
  • 【Java毕设源码分享】基于SpringBoot的农村留守儿童爱心网站的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 机器学习真实学习路径:避开环境、项目、数学三大断崖
  • 2026国际EMBA口碑排名盘点:五大高认可度项目深度测评
  • 告别单机调试:ROS1多机协同实战,让机器人A控制机器人B运动(基于Wheeltec底盘案例)
  • 2026年主播偷逃税事件的危机公关方案
  • TCD映射与簇代数在离散微分几何中的应用
  • 国产IDE崛起?实测MounRiver Studio对沁恒CH32V103/F103的友好度到底如何
  • RimWorld性能优化终极指南:如何用Performance-Fish让你的殖民地流畅运行
  • 体验人机协同编程:在快马平台如何让codex成为你的智能代码助手
  • Mac系统级ChatGPT集成:零感知调用的Shell服务方案