
1. 为什么这两个概念是Python进阶的分水岭把Python基础语法学完之后很多人会进入一个很尴尬的阶段列表推导、字典操作、文件读写这些都会了写出来的代码也能跑但总感觉差点意思。看别人写的源码动不动冒出个something或者看到yield这个关键字一脸懵。面试的时候十个问题有八个会绕到装饰器和生成器头上。说实话这两个概念并不是什么花哨的高级技巧它们解决的是日常开发里非常具体、非常常见的问题。装饰器解决的是在不修改原函数代码的前提下给函数增加额外能力的问题生成器解决的是数据量很大甚至无限时如何边算边用、不占内存的问题。这两件事只要你写Python早晚会碰上。这篇文章不会一上来就丢概念定义而是从最朴素的需求出发一步步推到装饰器和生成器的写法。零基础能跟着走有基础的人也能在中间捡到一些之前没注意的细节比如functools.wraps到底解决了什么、yield背后的执行流程、装饰器和生成器组合起来的实战写法。这些内容在官方文档里都查得到但文档不会告诉你哪些细节会坑你一下。2. 装饰器的地基先搞懂函数也是对象很多人学装饰器的时候卡住不是装饰器本身难而是前置概念缺了一块。装饰器的底层依赖两样东西函数可以作为变量传递以及闭包的存在。这两个都是Python函数式编程的基石今天把它们彻底说透。2.1 函数可以被赋值、传参和返回在Python里函数名本质上是一个变量名它绑定到一个函数对象。你写def hello():的时候相当于做了一次赋值操作把hello这个名字指向了一段可调用的代码。既然是变量就可以赋值给别人def hello(): return Hello, world! # 把函数对象赋值给另一个变量 hi hello print(hi()) # Hello, world! print(hello.__name__) # hello print(hi.__name__) # hello同一个函数对象注意这里有个初学者容易混淆的点hi hello后面没有括号。带括号是调用函数不带括号是引用函数本身。把函数作为参数传进另一个函数也是同样的道理def call_twice(func, arg): return func(arg) func(arg) def shout(word): return word.upper() print(call_twice(shout, hello)) # HELLOHELLOshout函数被当作参数传给call_twice在函数内部被调用了两次。这种把一个函数当参数传给另一个函数的写法叫做高阶函数。Python内置的map、filter、sorted的key参数都是这种用法。既然函数能作为参数传入那它能不能作为返回值呢当然可以。def make_multiplier(n): def multiplier(x): return x * n return multiplier times_3 make_multiplier(3) print(times_3(10)) # 30这段代码里make_multiplier内部定义了一个multiplier函数并且把它作为返回值丢了出来。times_3拿到的是multiplier这个函数对象调用times_3(10)就是在执行multiplier(10)。到这里你就已经写出一个简单的闭包了。2.2 闭包带着记忆的函数刚才那个make_multiplier的例子有个很巧妙的地方multiplier函数内部用到了n但这个n是外部函数make_multiplier的参数。按照普通直觉make_multiplier执行结束之后它的局部变量就应该被回收了那multiplier里用的n从哪来这就是闭包的核心机制当一个内层函数引用了外层函数的变量时Python会把外层函数的这个变量打包进内层函数让它跟着内层函数一起走。就算外层函数已经执行完毕内层函数依然记得那个值。你可以把闭包理解成一个函数背着一个小背包背包里装着它从外层带来的变量。times_3的背包里装着n3所以任何数乘以3如果再调用make_multiplier(5)得到的times_5背包里装的就是n5两者互不干扰。用__closure__属性可以验证这个背包的存在def make_multiplier(n): def multiplier(x): return x * n return multiplier times_3 make_multiplier(3) print(times_3.__closure__[0].cell_contents) # 3cell_contents存的就是n的当前值。到这里装饰器的两块地基就齐了函数可以作为变量传来传去内层函数能记住外层函数的变量。有了这两个能力装饰器就是水到渠成的事。3. 从需求出发手写第一个装饰器现在假装你有个很简单的需求项目里有好几个函数你想知道每个函数跑了多长时间。最朴素的办法是每个函数里加几行计时代码但这样又丑又重复而且侵入原函数逻辑。装饰器就是来解决这个问题的。3.1 最简装饰器的完整推导先写一个需要被增强的函数def process_data(): # 模拟耗时操作 total 0 for i in range(1, 1000000): total i return total如果要给这个函数加计时常规做法是改函数内部代码。但如果不允许改呢或者有十几个函数都要加呢这时候就可以写一个包装工厂接受一个函数返回一个包装过的新函数import time def time_it(func): def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) end time.perf_counter() print(f{func.__name__} 耗时 {end - start:.4f} 秒) return result return wrappertime_it做的事情很简单接收一个函数func定义一个新函数wrapper在wrapper里先记录开始时间然后调用原来的func再记录结束时间最后把func的返回值原样返回。*args, **kwargs保证了不管原函数接收什么参数这个包装函数都能原样透传。用法也直观process_data time_it(process_data) result process_data()这一行把process_data重新绑定到了wrapper上。以后再调用process_data()实际上执行的是wrapper()而wrapper内部会调用原来的函数逻辑。原来的函数代码没动过但行为被增强了——这就是装饰器不改代码增强功能的思想。每次都要写一行process_data time_it(process_data)太麻烦了Python提供的语法糖就是time_it def process_data(): total 0 for i in range(1, 1000000): total i return totaltime_it放在函数定义上方效果等同于process_data time_it(process_data)。这个语法糖在读代码时也很直观一眼就能看出这个函数被计时能力装饰过。3.2 functools.wraps一个必须养成的习惯直接写time_it有个隐患。因为process_data这个名字现在指向的是wrapper所以打印它的__name__会得到wrapper而不是process_data。很多调试工具、文档生成工具依赖函数的元信息名字变了就会出问题。print(process_data.__name__) # wrapper不是 process_data解决方法是使用functools.wrapsimport functools def time_it(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) end time.perf_counter() print(f{func.__name__} 耗时 {end - start:.4f} 秒) return result return wrapperfunctools.wraps(func)会把原函数的__name__、__doc__、__module__等元信息复制到wrapper上。这样装饰完之后函数的身份信息不会丢。有人觉得这一步无所谓但实际项目里被装饰的函数一旦要接入日志系统、Sphinx文档或者Flask路由元信息丢失就会造成很隐蔽的问题。我从一开始就建议加上functools.wraps成本几乎为零省掉的是后续排查问题的时间。3.3 带参数的装饰器再加一层函数的功夫有时候装饰器自己也想要参数比如只打印耗时超过1秒的函数。你可能会想着把time_it改成time_it(threshold1)但直接这么写会报错因为time_it(threshold1)会在装饰之前先执行返回的结果才是真正的装饰器。所以带参装饰器需要三层嵌套import functools import time def time_it(threshold0.1): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) end time.perf_counter() elapsed end - start if elapsed threshold: print(f{func.__name__} 耗时 {elapsed:.4f} 秒超过阈值 {threshold}) return result return wrapper return decorator time_it(threshold0.5) def slow_job(): time.sleep(1) slow_job() # slow_job 耗时 1.0002 秒超过阈值 0.5这层的逻辑关系是time_it(threshold0.5)先执行返回decoratordecorator接收被装饰的函数返回wrapper最终slow_job指向wrapper。三层的记忆顺序是最外层参数threshold被第二层记住第二层装饰器decorator里的func被第三层记住。用之前学的闭包知识拆解一下time_it(threshold0.5)执行完后threshold0.5这个值就被锁在了返回的decorator里decorator(slow_job)执行完后slow_job函数对象被锁在wrapper里。两层闭包叠在一起就实现了带参数的装饰器。4. 装饰器的实战场景权限校验、耗时统计、结果缓存理解了装饰器的写法之后更重要的是知道它适合用在哪。我整理三个最常用也最值得模仿的场景每一个都是实际项目里反复出现的需求。4.1 登录权限校验切面思想的经典落地Web应用里很多接口需要登录后才能访问。最原始的做法是在每个接口函数开头写一段检查当前用户是否登录的逻辑重复且容易漏。装饰器可以把这个逻辑抽出来放在一个地方import functools from flask import request, session, jsonify def login_required(func): functools.wraps(func) def wrapper(*args, **kwargs): # 假设登录后 session 里存了 user_id if not session.get(user_id): return jsonify({code: 401, msg: 请先登录}), 401 return func(*args, **kwargs) return wrapper app.route(/api/profile) login_required def profile(): user_id session[user_id] # 查询数据库返回个人信息把权限校验的公共逻辑放到装饰器里业务函数只写自己的核心逻辑代码立刻清爽不少。这也是一种典型的切面编程思路把横跨多个函数公共逻辑抽出来统一处理。权限校验的装饰器还经常跟角色绑定比如管理员才能访问某些接口。你完全可以把角色作为装饰器参数def require_role(role): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): user_role session.get(role) if user_role ! role: return jsonify({code: 403, msg: 没有权限}), 403 return func(*args, **kwargs) return wrapper return decorator与数据库交互的函数可以提纯和HTTP层解耦。这里以Flask为例只是为了方便理解换成Django或者FastAPI思路完全一样。4.2 耗时统计与日志记录观察系统行为除了计时装饰器也适合做统一日志。比如记录函数被谁调用、传入什么参数、返回什么结果这在排查线上问题时非常有用。import functools import logging logging.basicConfig(levellogging.INFO) def log_call(func): functools.wraps(func) def wrapper(*args, **kwargs): logging.info(f调用 {func.__name__}, args{args}, kwargs{kwargs}) try: result func(*args, **kwargs) logging.info(f{func.__name__} 返回 {result}) return result except Exception as e: logging.exception(f{func.__name__} 执行异常: {e}) raise return wrapper这里有个细节值得注意try/except里捕获异常后用了raise把它重新抛出去。装饰器不应该吞掉原函数的异常否则上层业务感知不到错误会导致更隐蔽的失败。日志记录只管观察不能改变原有行为。4.3 结果缓存用空间换时间的通用解法如果一个函数是纯函数——同样的输入必然得到同样的输出且没有副作用——那就可以用装饰器把结果缓存起来避免重复计算。下面这个实现虽然简单但思路很典型import functools import time def cache_result(func): cache {} functools.wraps(func) def wrapper(*args, **kwargs): # 简化处理只按位置参数做key实际项目需要考虑kwargs key args if key not in cache: cache[key] func(*args, **kwargs) return cache[key] return wrapper cache_result def fib(n): if n 2: return n return fib(n - 1) fib(n - 2) start time.time() print(fib(35)) # 9227465 print(f耗时 {time.time() - start:.4f}s)不加速的递归计算fib(35)可能要一秒多加了缓存之后每个入参只计算一次整个递归瞬间完成。这种缓存思路在很多框架里都被内置了Python 3.9起可以用functools.cache3.8及以下版本可以用functools.lru_cache。注意这里的cache字典定义在wrapper外面其实就是闭包背包里的变量。函数被多次调用cache会一直被保留这就是带着记忆的函数最直接的体现。4.4 类装饰器与装饰器类面向对象风格的实现装饰器不一定是函数只要是一个可调用对象就行。类的实例只要实现了__call__方法算可调用对象所以可以用类来实现装饰器import functools import time class TimeIt: def __init__(self, func): self.func func def __call__(self, *args, **kwargs): start time.perf_counter() result self.func(*args, **kwargs) print(f{self.func.__name__} 耗时 {time.perf_counter() - start:.4f}s) return result TimeIt def heavy_work(): time.sleep(0.5) heavy_work()用类写装饰器的好处是可以把一些复杂的状态封装成实例属性。比如你想统计某个函数被调用多少次用类装饰器会很方便class Counter: def __init__(self, func): self.func func self.count 0 def __call__(self, *args, **kwargs): self.count 1 print(f第 {self.count} 次调用) return self.func(*args, **kwargs)如果类本身需要接受参数那就在__init__里接收参数、在__call__里接收函数和函数式三层装饰器思路相同稍稍调整即可。5. 生成器的本质yield改变函数的执行方式装饰器是函数式思维的体现生成器则是惰性求值的典范。很多初学者第一次见到yield都觉得神秘觉得它是某种返回值但不结束函数的黑魔法。其实它跟return的差异从执行流程上就能解释清楚。5.1 先摆一个实际需求斐波那契数列假设要写一个函数产生前n个斐波那契数。最直觉的写法是用列表收集所有结果最后一起返回def fib_list(n): result [] a, b 0, 1 for _ in range(n): result.append(a) a, b b, a b return result numbers fib_list(1000000)这段代码能跑但如果你把n改成10000000内存会瞬间飙升。因为result列表把所有数值都存了下来。可问题是大多数使用场景下你并不需要一次性拿到全量数据而是逐个处理——打印、求和、取前几个。这时候一次算一个、用完一个扔一个的生成器就派上用场了。5.2 yield的暂停与恢复机制下面是用生成器实现的斐波那契def fib_gen(): a, b 0, 1 while True: yield a a, b b, a b这个函数只要一调用不会执行里面的任何代码而是立刻返回一个生成器对象。生成器对象支持用next()逐步取值f fib_gen() print(next(f)) # 0 print(next(f)) # 1 print(next(f)) # 1 print(next(f)) # 2每次执行next(f)函数体会从上一次yield的地方继续往下执行直到再次遇到yield把值交给外部然后再次暂停。这里有个对比可以用return相当于函数执行到此为止栈帧全部销毁yield相当于函数执行到一半存档下次next()时读档继续。这个存档存的是整个局部状态包括a、b当前的值。所以生成器不需要像列表那样把所有值记在容器里它只记住当前进行到哪了。用for循环也可以遍历生成器for num in fib_gen(): if num 100: break print(num)for循环本质上是反复调用next()直到捕获StopIteration异常。生成器函数如果执行到函数末尾会自动抛出StopIteration来通知外部没值了。5.3 生成器表达式把列表推导换成惰性版本列表推导很直观但会一次性创建完整列表。如果你只需要其中一个或者数据量很大可以换成生成器表达式语法只是把方括号换成圆括号# 列表推导立即计算所有平方并存入列表 squares_list [x * x for x in range(1000000)] # 生成器表达式惰性计算每次取一个 squares_gen (x * x for x in range(1000000)) print(type(squares_list)) # class list print(type(squares_gen)) # class generator两者在内存占用上有质的区别。跑一下这个例子就能直观感受到import sys print(sys.getsizeof(squares_list)) # 列表占用的字节数通常很大 print(sys.getsizeof(squares_gen)) # 生成器对象本身很小大概几十字节生成器表达式的内存开销是一个常数因为数据在遍历时才逐个生成。结合sum()、max()、min()这类只关心聚合结果的函数生成器表达式比列表推导更省内存total sum(x * x for x in range(10000000))sum内部会不断next()生成器表达式逐个累加整个过程不需要在内存里保存一个超长的平方列表。5.4 迭代器与可迭代对象的关系搞懂这两个词以后看文档、源码会顺很多。一个对象如果可以被for循环遍历它就是一个可迭代对象比如列表、字符串、字典、元组。一个对象如果能被next()逐个取值它就是一个迭代器生成器就是典型的迭代器。关键在于可迭代对象不一定自己就是迭代器。比如列表可以被for遍历但你直接next([1,2,3])会报错因为列表没有__next__方法。for循环在遍历列表时内部会先调用iter()把列表转换成一个迭代器再对迭代器不断next()。下面这段代码可以验证lst [1, 2, 3] it iter(lst) print(next(it)) # 1 print(next(it)) # 2 print(next(it)) # 3 print(next(it)) # StopIteration生成器天然实现了迭代器协议所以可以直接被for和next()使用。这也是为什么我们说生成器是迭代器的一种。6. 生成器进阶从数据管道到协程雏形yield不止能向外吐数据还能从外部收数据。这一点让生成器的能力远超省内存的列表它可以作为协程的雏形实现简单的协作式多任务。6.1 send从外部向生成器传值生成器对象有一个send()方法可以往里传值。接收端在yield表达式的返回值处拿到这个值而不是直接忽略它。def echo(): received while True: received yield received if received quit: break g echo() print(next(g)) # 启动生成器此时 received 是空字符串 print(g.send(hello)) # hello print(g.send(world)) # world g.send(quit) # 会触发 StopIteration因为循环被 break 了第一次用next(g)启动是因为最初生成器还没执行到yield第一次send会尝试向一个未起始的生成器传值会报错。所以惯例是先next()一次让它跑到第一个yield处暂停。send的价值在于生成器不再只是单向产出数据它可以跟外部交互——外部给它一个值它处理后再产出新值。很多协程框架比如早期的asyncio调度器就是基于这个能力设计的。6.2 yield from委托给另一个生成器如果在一个生成器里需要让另一个生成器来处理一段数据可以用yield from把迭代过程委托出去def sub_gen(): yield 1 yield 2 def main_gen(): yield 0 yield from sub_gen() yield 3 for value in main_gen(): print(value) # 0 1 2 3yield from简化了手动迭代子生成器的写法也自动处理了子生成器的返回值、异常传递等细节。在实现复杂数据管道时yield from能让代码结构清晰很多。6.3 无限序列与大数据场景生成器在理论上是无穷的它不存储数据所以无限并没有内存问题。业务里最常见的还是处理大文件。比如一个好几GB的日志文件你用read()一次性读入内存直接爆。用readline()循环可以但更优雅的是自己写一个逐行产出的函数配合生成器处理def read_large_file(file_path): with open(file_path, r, encodingutf-8) as f: for line in f: yield line.strip() for line in read_large_file(huge.log): # 对每一行做处理 if ERROR in line: process_error(line)for line in f本身就会按行惰性读取加上yield外层for每取一行文件才会读一行外层不取文件就不读。整个文件不会一次性进入内存这是处理大数据的基本功。6.4 close和throw提前终止与异常注入生成器对象还有两个方法close()用于提前关闭生成器之后继续next()会抛StopIterationthrow()用于在生成器暂停的位置注入一个异常让生成器内部捕获处理。def safe_counter(): i 0 try: while True: yield i i 1 except GeneratorExit: print(生成器被关闭) except ValueError: print(捕获到外部注入的异常) c safe_counter() print(next(c)) # 0 c.throw(ValueError(外部异常)) # 捕获到外部注入的异常 c.close() # 生成器被关闭这两个方法在资源清理、超时控制等场景有实用价值。不过日常写业务代码时用得不多知道存在即可真遇到需求时能想到有这条路径就好。7. 装饰器与生成器合用一个完整的实战案例很多教程把装饰器和生成器分开讲实际项目里两者经常一起出现。这里我写一个完整可运行的例子把functools.lru_cache作为装饰器来缓存生成器的结果实现一个惰性但带缓存的逻辑。场景有一个函数会从远程接口拉数据数据量很大返回一个生成器逐条产出。为了减少请求次数想加缓存但缓存一个生成器直接存是不行的——生成器一旦消费完就没了第二次遍历拿不到数据。解决思路是装饰器缓存第一次完整遍历后得到的元组后续请求直接返回可迭代的元组。import functools import time def cache_stream(func): cache {} functools.wraps(func) def wrapper(*args, **kwargs): key (args, tuple(sorted(kwargs.items()))) if key not in cache: # 触发完整遍历缓存成不可变的元组 cache[key] tuple(func(*args, **kwargs)) return iter(cache[key]) return wrapper cache_stream def fetch_records(source_url): 模拟从远程接口分页拉取大量数据 for page in range(1, 5): time.sleep(0.2) print(f拉取第 {page} 页) for record_id in range(page * 10, page * 10 10): yield f{source_url}-record-{record_id} print(第一次遍历) for rec in fetch_records(api.v1): pass print(第二次遍历) for rec in fetch_records(api.v1): pass运行结果会显示第二次遍历时fetch_records内部的print一次都没执行说明结果是直接命中了缓存没有重复发出网络请求。这个模式很实用对外暴露一个假装是生成器但实际上有缓存的接口。再举个例子把装饰器和生成器组合成一个计数管道。装饰器负责统计数据量生成器负责逐条产出def count_iter(func): total 0 functools.wraps(func) def wrapper(*args, **kwargs): nonlocal total gen func(*args, **kwargs) for item in gen: total 1 print(f当前产出第 {total} 条) yield item return wrapper count_iter def request_data(): for i in range(5): yield i * 2 for value in request_data(): print(value)装饰器里有个nonlocal total它修改了闭包外层函数的变量。这个细节相当于给之前的闭包知识又加深了一层——装饰器内部的闭包是可以携带可变状态的。8. 我踩过的坑你大概率也会踩这部分内容是我自己写装饰器和生成器时真实遇到过的、以及身边同事反复踩的问题。每一个都值得你留意。8.1 装饰器叠加顺序当一个函数叠多个装饰器时顺序很关键。装饰器是从下往上应用的执行顺序却是从上往下的。def decorator_a(func): functools.wraps(func) def wrapper(*args, **kwargs): print(A enter) result func(*args, **kwargs) print(A exit) return result return wrapper def decorator_b(func): functools.wraps(func) def wrapper(*args, **kwargs): print(B enter) result func(*args, **kwargs) print(B exit) return result return wrapper decorator_a decorator_b def hello(): print(hello) hello()输出顺序是A enter B enter hello B exit A exit所以如果你想控制逻辑的包裹层次要把最外层的装饰器写在上边把最贴近原函数的写在下面。比如既有计时的装饰器又有权限校验的装饰器你希望先校验还是先计时就直接决定了叠加顺序。8.2 生成器只能遍历一次生成器是一次性的。第一次for遍历干净之后再遍历同一个生成器对象得到的是空的。这跟列表很不一样很容易踩。def numbers(): for i in range(5): yield i gen numbers() print(list(gen)) # [0, 1, 2, 3, 4] print(list(gen)) # []第二次调用list(gen)时生成器已经在第一次遍历中走到了尽头继续next()只会得到StopIterationlist把它当作空列表处理。正确的做法是需要多次遍历时要么每次重新调用生成器函数创建一个全新的生成器对象要么像前面实战案例那样把数据缓存成可重用的容器。8.3 装饰器里忘了return这是新手最容易犯的错。wrapper里调用了func拿到了result如果忘记return result外部拿到的永远是None。def bad_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) # 忘记 return result return wrapper bad_decorator def add(a, b): return a b print(add(1, 2)) # None我自己早期写过不少这种有日志没返回值的装饰器函数计算结果被静默丢掉了。排查了很久才发现是装饰器的问题。所以写装饰器时在wrapper返回前脑子里过一遍我到底有没有把原函数的返回值透传出去8.4 生成器函数里用return的语义在生成器函数里写上return value不会让这个value被当作普通结果返回而是会体现在StopIteration异常中。例如def gen_with_return(): yield 1 yield 2 return 完成 g gen_with_return() print(next(g)) # 1 print(next(g)) # 2 try: next(g) except StopIteration as e: print(e.value) # 完成Python 3.3以后的StopIteration异常对象带有value属性存的就是生成器函数里return的值。这个特性在yield from场景中更常用因为yield from会把子生成器的返回值传出来。平时写生成器时不写返回值完全没影响但知道这个机制能避免某些迷惑行为。8.5 在类里用装饰器时注意self如果用你写的装饰器去装饰类的方法wrapper的第一个参数会接收到self实例对象。如果装饰器内部不处理这个参数直接透传通常没问题。但如果你在装饰器里假设参数列表是固定的就很容易出错。def method_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # args 第一个是 self return func(*args, **kwargs) return wrapper class Demo: method_decorator def greet(self, name): return fHello, {name} print(Demo().greet(Tom)) # Hello, Tom如果你在wrapper里想自己判断参数一定要记得args的第一个元素是self。另外如果你的装饰器需要访问实例的属性可以在wrapper里通过args[0]拿到self然后访问。这属于装饰器的高级玩法我实际中在写内部框架时用过写业务代码时尽量少搞这么复杂简单清晰的装饰器才是好装饰器。8.6 别把生成器一次性转成列表生成器的好处就是省内存、惰性执行。如果你拿到一个生成器后马上list()转成列表前面的优势就全没了。真要转说明你本可以直接用列表推导。遇到生成器时先想一想真的需要全量数据吗还是只需要按顺序处理一遍如果是后者直接用for循环遍历生成器就行。9. 从零基础到进阶的学习路径小结很多人学装饰器和生成器是为了应付面试背几个代码模板就觉得会了。但从我的实际经验看真正能让你会的路径是先理解函数式基础再写几个装饰器然后在真实代码里用上几次踩过几次坑印象就牢了。装饰器的学习核心在于理解函数也是对象和闭包保存状态。只要这两个地基扎实装饰器不过是一层薄薄的语法糖。生成器的学习核心在于理解惰性求值和暂停恢复机制。你把yield的执行细节吃透生成器的所有高级用法都是在这个机制上叠加。拿一个问题检验一下自己写出下面的代码你觉得输出是什么def make_multiplier(n): def multiplier(x): return x * n return multiplier multipliers [make_multiplier(i) for i in range(3)] for m in multipliers: print(m(1))如果答案是0 1 2那说明你的直觉基本准确了。但如果你照抄这段代码跑一遍可能会得到2 2 2。原因涉及一个Python经典面试题——闭包变量延迟绑定。在make_multiplier这个函数写法里每次调用make_multiplier(i)时i会被锁定所以结果是0 1 2。但如果把写法改成下面这样结果就变了def make_multipliers(): multipliers [] for i in range(3): def multiplier(x): return x * i multipliers.append(multiplier) return multipliers for m in make_multipliers(): print(m(1)) # 2 2 2因为所有multiplier函数共享了同一个闭包变量i循环结束时i2所以都返回2。这个坑值得亲自跑一遍亲眼看看。写到这里我想起自己第一次搞懂装饰器时的心情原来代码可以写得这么优雅。生成器第一次帮你省下几GB内存时你也会有一种打开新世界大门的感觉。这两个概念不是面试的八股文而是每个写Python的人都能从里面获得实际收益的工具。如果你在读完这篇文章后愿意打开编辑器把我给的代码例子亲手敲一遍再改动几个参数看看效果那这篇文字花的时间就值了。