Python 面试高频:装饰器、迭代器、生成器和上下文管理器一次讲清
Python 面试高频:装饰器、迭代器、生成器和上下文管理器一次讲清
开场:这些题为什么总被问?
如果面试官问 Python 基础,除了昨天讲过的对象、引用、可变默认参数、浅拷贝、GIL,后面大概率会追这几个问题:
- 装饰器本质是什么?
- 闭包为什么能记住外层变量?
functools.wraps有什么用?- 迭代器和可迭代对象有什么区别?
- 生成器为什么省内存?
yield到底暂停了什么?with语句背后发生了什么?- 上下文管理器为什么适合管理文件、连接和锁?
这些问题不是为了考你会不会背术语,而是看你有没有理解 Python 的几个核心设计:
函数可以像对象一样传递,遍历依赖统一协议,惰性计算可以保存执行现场,资源释放应该交给明确的进入和退出边界。
这句话听起来长,拆开就是今天这篇文章的四条线:
- 装饰器和闭包:函数也是对象,函数可以包函数。
- 迭代器协议:
for循环背后不是魔法,而是iter()和next()。 - 生成器:
yield让函数可以暂停、恢复、逐个产出数据。 - 上下文管理器:
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)returnresultreturnwrappertimer已经执行结束了,为什么wrapper后面还能用func?
答案就是闭包。
闭包是指内部函数引用了外部函数作用域里的变量,并且这个内部函数被返回或传递到外部继续使用。
看一个更小的例子:
defmake_counter():count=0definc():nonlocalcount count+=1returncountreturninc counter=make_counter()print(counter())# 1print(counter())# 2print(counter())# 3make_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)returnwrapperwraps会把原函数的一些元信息复制到wrapper上,比如__name__、__doc__、__module__、__annotations__等。
这不是为了“好看”,而是为了让包装后的函数尽量像原函数。
面试回答模板
装饰器会用
wrapper替换原函数,如果不加functools.wraps,外部看到的函数名、文档、注解等元信息可能变成wrapper。wraps的作用是把原函数的元信息复制到包装函数上,方便调试、文档生成和框架反射。
四、迭代器: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) | 从迭代器里取下一个值 |
当没有更多数据时,迭代器会抛出StopIteration,for循环捕获它并结束循环。
可迭代对象和迭代器有什么区别?
一句话:
可迭代对象是“能被
iter()转成迭代器的对象”;迭代器是“能被next()不断取值的对象”。
常见可迭代对象:
listtupledictsetstr- 文件对象
- 生成器对象
迭代器对象要实现两个方法:
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))# 2nums是可迭代对象,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()返回一个值,没有值时抛出StopIteration。for循环本质上就是先调用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时,函数会:
- 产出当前值
i。 - 暂停在这一行。
- 保存当前局部变量和执行位置。
- 等下一次
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()))yield和return有什么关系?
生成器函数里可以写return,但它不是像普通函数那样返回最终列表。
defgen():yield1returnyield2return会结束生成器。外部继续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,表示异常被处理,不再向外抛;返回False或None,异常会继续向外传播。
日常写资源管理时,大多数情况下应该让异常继续抛出去,不要随便吞异常。
用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()不断取值,没有数据时抛出StopIteration。for循环本质上就是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
