Python 进阶:闭包、装饰器、迭代器与生成器
通过连续的概念链理解闭包、装饰器、迭代器和生成器的实现原理与使用方式。
这四个概念层层递进:闭包是基础 → 装饰器建立在闭包之上 → 迭代器是一套协议 → 生成器是实现迭代器的简便方式。
一、闭包 Closure
是什么
嵌套函数 + 内层函数引用了外层变量 + 外层返回内层函数。 内层函数"记住"了它定义时所在的环境,即使外层函数已经执行完毕。
def make_counter():
count = 0
def counter():
nonlocal count # 修改外层变量必须声明 nonlocal
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3count 在 make_counter 返回后本该消失,但因为 counter 还引用着它,被"保活"了。
有什么用
核心价值:让函数携带私有状态,又不用写一个完整的类。
| 用途 | 说明 |
|---|---|
| 工厂函数 | 批量生产"定制版"函数 |
| 装饰器 | 装饰器全靠闭包记住被包装的函数 |
| 封装私有状态 | 外部完全访问不到,天然私有变量 |
| 延迟计算/回调 | 把"要做的事+数据"打包,等需要时再调用 |
# 工厂函数例子
def power(exp):
def f(base):
return base ** exp
return f
square = power(2) # square 记住了 exp=2
cube = power(3) # cube 记住了 exp=3
print(square(5), cube(5)) # 25 125⚠️ 经典坑:延迟绑定
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] ← 不是 [0, 1, 2]!原因:闭包记住的是变量本身,不是变量当时的值。循环结束时 i 已经是 2,三个 lambda 调用时都去查 i,全得到 2。
创建阶段:造三个 lambda,每个都写着"返回变量 i"(不是值)
调用阶段:循环已结束,i = 2,三个 lambda 都返回 2解法:用默认参数在定义时立即"冻住"值:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2] ✓与 Java 对比
Java 匿名内部类捕获变量必须是 final(不可修改),Python 通过 nonlocal 允许修改捕获的变量。
💡 闭包是"穷人的对象",对象是"富人的闭包"。 状态简单时用闭包,状态复杂、方法多时用类。
二、装饰器 Decorator
作用:不修改原函数给原函数添加新功能
核心公式
@deco 等价于 def f(): ...
def f(): ... f = deco(f)@timer 就是 work = timer(work) 的语法糖,没有任何额外魔法。
理解步骤
第一步:函数可以当变量传递
def hello():
print("hi")
f = hello # 不加括号 = 函数本身(不执行)
f() # 加括号 = 执行第二步:函数可以接收函数、返回函数
def timer(func): # ① 接收被装饰的函数
def wrapper(): # ② 造一个新函数
print("开始")
func() # ③ 调用原函数(闭包记住了 func)
print("结束")
return wrapper # ④ 返回新函数(不加括号!)第三步:@timer 自动执行替换
@timer
def work():
print("干活")
# Python 自动执行:work = timer(work)
# work 这个名字现在指向 wrapper,原函数被 wrapper 包住了
work()
# 开始
# 干活
# 结束完整写法(含 functools.wraps)
import functools
def timer(func):
@functools.wraps(func) # 把原函数的名字、文档等复制给 wrapper
def wrapper(*args, **kwargs): # *args/**kwargs 接收任意参数
import time
start = time.time()
result = func(*args, **kwargs) # 原样转发参数,接住返回值
print(f"{func.__name__} 耗时 {time.time()-start:.4f}s")
return result # 别忘了返回,否则原函数返回值丢失
return wrapper为什么要加 functools.wraps
# 不加的话:
print(work.__name__) # 'wrapper' ← 名字被覆盖了
print(work.__doc__) # None ← 文档也丢了
# 加了之后:
print(work.__name__) # 'work' ← 正常
print(work.__doc__) # 原函数的文档习惯:写装饰器时,给 wrapper 总加上
@functools.wraps(func)。
带参数的装饰器(三层嵌套)
def repeat(n): # 第一层:接收装饰器参数
def decorator(func): # 第二层:接收被装饰的函数
@functools.wraps(func)
def wrapper(*args, **kwargs): # 第三层:实际执行逻辑
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # 先执行 repeat(3) 得到 decorator,再装饰函数
def greet():
print("hi")多个装饰器的顺序
@timer
@logger
def work(): ...
# 等价于:
work = timer(logger(work))
# 离函数近的先套(内层),离函数远的后套(外层)三、迭代器 Iterator
两个概念别混淆
| 实现了什么 | 能被 for 用 | 能被 next() 用 | 能重复遍历 | |
|---|---|---|---|---|
| 可迭代对象 (list/str/dict) | 只有 __iter__ |
✅ | ❌ | ✅ |
| 迭代器 | __iter__ + __next__ |
✅ | ✅ | ❌(取完就空) |
迭代器一定是可迭代对象,可迭代对象不一定是迭代器。
类比理解
可迭代对象 = 一本书(有内容,能被翻阅)
迭代器 = 书签(记着"翻到哪页了",能一页页往后翻)你不能直接"翻书",先放书签(iter()),再靠书签一页页翻(next())。
底层机制
# for x in [1, 2, 3] 的真实过程
_iter = iter([1, 2, 3]) # 先拿迭代器
while True:
try:
x = next(_iter) # 反复取下一个
# 执行循环体...
except StopIteration: # 取完了就停
break手写迭代器类
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # 迭代器返回自身
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
for x in Countdown(3):
print(x) # 3 2 1迭代器 vs C++ 指针
| Python 迭代器 | C++ 指针/迭代器 | |
|---|---|---|
| 移动方向 | 只能向前 | 可前后跳跃 |
| 地址概念 | 无,封装了位置 | 本质是内存地址 |
| 数据来源 | 可以"边走边生成" | 必须指向已存在的内存 |
最大区别:Python 迭代器可以无中生有(惰性生成),不需要提前把数据放在内存里。
四、生成器 Generator
是什么
生成器 = 用 yield 实现迭代器的语法糖。不用写类,不用写 __iter__ 和 __next__,函数里有 yield 就自动变成生成器。
调用生成器函数 ≠ 执行函数,而是造出一个迭代器对象。
def countdown(n):
while n > 0:
yield n # 执行到这里暂停,把 n 返回出去
n -= 1 # 下次 next() 才从这里继续
g = countdown(3) # 一行代码都没执行!g 是生成器对象
print(g) # <generator object countdown at 0x...>
print(next(g)) # 现在才开始执行,跑到 yield 3,暂停,返回 3
print(next(g)) # 从 n -= 1 继续,yield 2,暂停,返回 2
print(next(g)) # yield 1
print(next(g)) # while 不成立,StopIterationyield vs return
return |
yield |
|
|---|---|---|
| 执行后 | 函数结束,状态消失 | 函数冻住,状态保留 |
| 再次调用 | 从头开始 | 从上次暂停处继续 |
| 返回次数 | 只能一次 | 可以多次 |
核心特性:惰性求值
import sys
a = range(100_000_000) # 惰性,不占内存
b = list(range(100_000_000)) # 真实列表,占几百 MB
print(sys.getsizeof(a)) # 48 字节
print(sys.getsizeof(b)) # 800+ MB实用例子
例1:无限序列(普通列表做不到)
def fibonacci():
a, b = 0, 1
while True: # 永远不停也没关系
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(8):
print(next(fib), end=' ') # 0 1 1 2 3 5 8 13例2:大文件逐行处理
def read_lines(filename):
with open(filename) as f:
for line in f:
yield line.strip() # 每次只有一行在内存里
for line in read_lines("huge.log"):
process(line)例3:生成器表达式(把 [] 换成 ())
squares_list = [x*x for x in range(10)] # 立刻算出全部,存进内存
squares_gen = (x*x for x in range(10)) # 惰性,用时才算
# 直接传给函数,不需要先建列表
print(sum(x*x for x in range(10))) # 285例4:yield from 委托
def chain(*iterables):
for it in iterables:
yield from it # 把 it 里的值一个个替我 yield 出去
print(list(chain([1, 2], [3, 4]))) # [1, 2, 3, 4]何时用列表,何时用生成器
| 场景 | 用什么 |
|---|---|
| 需要多次遍历 | 列表 [] |
| 需要随机访问(索引) | 列表 [] |
| 只遍历一次 | 生成器 () |
| 数据量大、节省内存 | 生成器 () |
| 无限序列 | 生成器(列表做不到) |
五、总览:四个概念的关系
闭包
└── 让函数记住外层变量(基础能力)
装饰器
└── 吃进一个函数、吐出一个加强版函数
└── 靠闭包记住原函数
└── @语法糖 = f = deco(f)
迭代器协议
└── __iter__ + __next__
└── for 循环的底层机制
└── 取完就空,不可倒退
生成器
└── 用 yield 自动实现迭代器
└── 函数有 yield → 调用时返回生成器对象
└── 惰性求值,走一步生产一步一句话串联:闭包让函数携带状态 → 装饰器用闭包包装函数 → 迭代器是"能逐个取值"的协议 → 生成器用
yield轻松造出迭代器。