Python装饰器、生成器、上下文管理器:3个必会的"魔法"机制
关键词:Python进阶、装饰器、生成器、上下文管理器、with语句、yield、闭包
一、引言:为什么是这三个?
如果你已经写了一段时间的Python,大概率见过这样的代码:
less
@timer
def process_data():
...
for chunk in read_large_file("huge.csv"):
...
with open("data.txt") as f:
content = f.read()
这三段代码分别用到了装饰器 、生成器 和上下文管理器。
它们看起来像"语法糖",但实际上,它们是Python设计哲学的核心体现------把复杂留给自己,把简洁留给用户。
这篇文章的目标:从"会用"到"理解",再到"能自己写"。
二、装饰器(Decorator):给函数"穿衣服"
2.1 什么是装饰器?
一句话:装饰器是一个接收函数、返回函数的函数。
它的作用是在不修改原函数代码的前提下,给函数增加新功能。
打个比方:你有一部手机(原函数),你想防水,但不想拆开手机改装。怎么办?买个防水壳套上去。这个防水壳就是装饰器。
2.2 从最朴素的需求开始
假设你想统计一个函数的执行时间:
lua
import time
def slow_function():
time.sleep(1)
print("Done!")
# 朴素写法:在函数里加代码
def slow_function():
start = time.time()
time.sleep(1)
print("Done!")
end = time.time()
print(f"耗时: {end - start:.2f}秒")
问题来了:如果你有20个函数都要统计时间,难道每个都改一遍?
2.3 用装饰器解决
python
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # 调用原函数
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.2f}秒")
return result
return wrapper
# 使用方式一:函数式调用
def slow_function():
time.sleep(1)
print("Done!")
slow_function = timer(slow_function)
slow_function()
输出:
Done!
slow_function 耗时: 1.00秒
2.4 语法糖:@符号
上面那种 slow_function = timer(slow_function) 的写法太丑了。Python提供了 @ 语法糖:
less
@timer
def slow_function():
time.sleep(1)
print("Done!")
slow_function()
@timer 就等于 slow_function = timer(slow_function)。
就这么简单。
2.5 带参数的装饰器
如果我想让装饰器本身也能接收参数呢?比如控制日志级别:
python
def log(level="INFO"):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{level}] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log(level="DEBUG")
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
csharp
[DEBUG] 调用 greet
Hello, Alice!
三层嵌套,从外到内:
scss
log(level) → 返回 decorator
decorator(func) → 返回 wrapper
wrapper(...) → 执行增强逻辑 + 原函数
2.6 保留原函数信息
你可能会发现,装饰后的函数"丢失"了原函数的名字和文档:
python
@timer
def slow_function():
"""这是一个很慢的函数"""
time.sleep(1)
print(slow_function.__name__) # 输出: wrapper ← 不对!
print(slow_function.__doc__) # 输出: None ← 不对!
解决办法:用 functools.wraps:
python
import functools
def timer(func):
@functools.wraps(func) # ← 加上这行
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.2f}秒")
return result
return wrapper
2.7 实用场景
| 场景 | 装饰器示例 |
|---|---|
| 性能统计 | @timer |
| 权限校验 | @login_required |
| 缓存结果 | @functools.lru_cache |
| 重试机制 | @retry(max_attempts=3) |
| 路由注册 | @app.route("/home") (Flask) |
2.8 内置装饰器
Python自带了几个常用的:
ruby
# 缓存:同样的参数只计算一次
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# 把方法变成属性调用
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
c = Circle(5)
print(c.area) # 像属性一样调用,不用加括号
# 类方法
class MyClass:
@classmethod
def from_string(cls, s):
...
@staticmethod
def helper():
...
三、生成器(Generator):懒惰是美德
3.1 什么是生成器?
一句话:生成器是一个"按需生产"的可迭代对象。
普通函数用 return 一次性返回所有结果;生成器用 yield 每次返回一个结果,然后"暂停",等你需要下一个时再"恢复"。
3.2 从列表的问题说起
ini
# 生成1到100万的平方
def get_squares(n):
result = []
for i in range(n):
result.append(i ** 2)
return result
squares = get_squares(1_000_000) # 内存中存了100万个数字
问题:如果只需要遍历一次,这100万个数字占用了大量内存(约8MB),而且你可能在第10个就break了,后面999990个白算了。
3.3 用生成器改写
python
def get_squares_gen(n):
for i in range(n):
yield i ** 2 # ← 关键:yield不是return
# 使用
gen = get_squares_gen(1_000_000)
print(gen) # <generator object ...>
for sq in gen:
print(sq)
if sq > 100:
break
区别:
| 普通函数 | 生成器 | |
|---|---|---|
| 返回方式 | return |
yield |
| 返回次数 | 一次 | 多次 |
| 内存占用 | 存所有结果 | 只存当前值 |
| 执行方式 | 从头跑到尾 | 跑到yield暂停,下次从暂停处继续 |
3.4 生成器的工作原理
python
def counter():
print("开始")
yield 1
print("继续")
yield 2
print("结束")
yield 3
c = counter()
print(next(c)) # 输出: 开始 \n 1
print(next(c)) # 输出: 继续 \n 2
print(next(c)) # 输出: 结束 \n 3
print(next(c)) # StopIteration 异常
关键理解:
- 调用
counter()时,函数体并没有执行 - 第一次
next(c),执行到第一个yield,返回1,暂停 - 第二次
next(c),从暂停处继续,执行到第二个yield,返回2,暂停 - 以此类推,直到没有更多
yield,抛出StopIteration
3.5 生成器表达式
和列表推导式很像,只是把 [] 换成 ():
ini
# 列表推导式
squares_list = [x**2 for x in range(1000000)] # 立即生成所有
# 生成器表达式
squares_gen = (x**2 for x in range(1000000)) # 按需生成
3.6 用生成器处理大文件
这是生成器最经典的场景:
python
def read_large_file(filepath, chunk_size=8192):
"""逐行读取大文件,不占用大量内存"""
with open(filepath, 'r') as f:
for line in f:
yield line.strip()
# 处理10GB的日志文件
for line in read_large_file("huge_log.txt"):
if "ERROR" in line:
print(line)
3.7 管道式数据处理
生成器可以串联,形成处理管道:
python
def read_lines(filepath):
with open(filepath) as f:
for line in f:
yield line.strip()
def filter_error(lines):
for line in lines:
if "ERROR" in line:
yield line
def extract_time(lines):
for line in lines:
yield line.split()[0] # 假设时间在第一列
# 管道串联
lines = read_lines("app.log")
errors = filter_error(lines)
times = extract_time(errors)
for t in times:
print(t)
每个生成器都是惰性的,数据像流水线一样逐条处理,内存占用极低。
3.8 send() 和 yield 的高级用法
生成器不仅能"产出"值,还能"接收"值:
python
def accumulator():
total = 0
while True:
value = yield total # 产出total,同时接收value
if value is None:
break
total += value
acc = accumulator()
next(acc) # 启动生成器,执行到第一个yield
print(acc.send(10)) # 发送10,total=10,产出10
print(acc.send(20)) # 发送20,total=30,产出30
print(acc.send(5)) # 发送5,total=35,产出35
这是协程(coroutine)的基础,也是 async/await 的前身。
四、上下文管理器(Context Manager):善始善终
4.1 什么是上下文管理器?
一句话:上下文管理器保证你"进门时开灯,出门时关灯"。
最常见的用法就是 with 语句。
4.2 从资源泄漏说起
ini
# 打开文件但不关闭------资源泄漏!
f = open("data.txt", "r")
content = f.read()
# 如果这里抛异常了,f.close()永远不会执行
f.close()
改进版:
ini
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close() # 确保关闭
写起来啰嗦。用 with:
csharp
with open("data.txt", "r") as f:
content = f.read()
# 文件自动关闭,即使里面抛异常
4.3 自己写一个上下文管理器
方法一:类实现(enter 和 exit)
python
class Timer:
def __enter__(self):
self.start = time.time()
return self # ← with语句中as后面的就是这个返回值
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.time()
print(f"耗时: {self.end - self.start:.2f}秒")
# 返回False则异常继续传播,返回True则吞掉异常
return False
# 使用
with Timer():
time.sleep(1)
方法二:用 @contextmanager 装饰器(更简洁)
python
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
yield # ← yield之前的代码相当于__enter__
end = time.time() # ← yield之后的代码相当于__exit__
print(f"耗时: {end - start:.2f}秒")
# 使用
with timer():
time.sleep(1)
对比:
| 方式 | 代码量 | 灵活性 | 适合场景 |
|---|---|---|---|
| 类实现 | 多 | 高(可处理异常细节) | 复杂资源管理 |
| @contextmanager | 少 | 中 | 简单场景 |
4.4 实用场景
场景一:临时切换工作目录
less
@contextmanager
def cd(path):
old_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_dir)
with cd("/tmp"):
# 在/tmp目录下工作
print(os.getcwd())
# 自动回到原目录
场景二:数据库连接
less
@contextmanager
def get_db_connection():
conn = psycopg2.connect("dbname=test user=postgres")
try:
yield conn
finally:
conn.close()
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
场景三:抑制异常
python
from contextlib import suppress
# 不想让FileNotFoundError打断程序
with suppress(FileNotFoundError):
os.remove("temp.txt")
# 文件不存在也不报错
4.5 contextlib 工具箱
python
from contextlib import closing, redirect_stdout, ExitStack
# closing:给没有close()的对象加上上下文管理
from urllib.request import urlopen
with closing(urlopen("http://example.com")) as page:
content = page.read()
# redirect_stdout:临时重定向输出
with redirect_stdout(open("output.txt", "w")):
print("这段文字写到了文件里")
# ExitStack:动态管理多个上下文
with ExitStack() as stack:
files = [stack.enter_context(open(f"file{i}.txt")) for i in range(5)]
# 5个文件全部自动关闭
五、三者结合:一个完整案例
把装饰器、生成器、上下文管理器放在一起,写一个带性能监控的大文件处理工具:
python
import time
import functools
from contextlib import contextmanager
# ========== 装饰器:性能监控 ==========
def monitor(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
with timer(): # ← 上下文管理器
return func(*args, **kwargs)
return wrapper
# ========== 上下文管理器:计时 ==========
@contextmanager
def timer():
start = time.time()
yield
end = time.time()
print(f"⏱️ [{timer.__module__}] 总耗时: {end - start:.2f}秒")
# ========== 生成器:逐行读取 ==========
def read_chunks(filepath, chunk_size=1024):
with open(filepath, 'r') as f: # ← 上下文管理器
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk # ← 生成器
# ========== 组合使用 ==========
@monitor
def count_words_in_large_file(filepath):
"""统计大文件中的单词数"""
total_words = 0
for chunk in read_chunks(filepath): # ← 消费生成器
total_words += len(chunk.split())
return total_words
# 使用
result = count_words_in_large_file("huge_text.txt")
print(f"单词总数: {result}")
输出:
makefile
⏱️ 总耗时: 2.34秒
单词总数: 158392
这个例子展示了什么?
@monitor装饰器给函数加了计时能力,不需要改函数内部read_chunks生成器让大文件处理不爆内存with open()上下文管理器确保文件正确关闭- 三者各司其职,组合在一起优雅而强大
六、对比总结
| 机制 | 核心关键字 | 本质 | 解决的问题 |
|---|---|---|---|
| 装饰器 | @ |
函数嵌套 | 不修改原代码增加功能 |
| 生成器 | yield |
暂停/恢复 | 大数据集的惰性处理 |
| 上下文管理器 | with |
资源生命周期 | 确保资源正确释放 |
心智模型
scss
┌─────────────────────────────────────────────┐
│ 装饰器 = 给函数"套壳" │
│ ┌─────────┐ │
│ │ 新功能 │ ← 计时、日志、权限... │
│ │┌────────│────────┐ │
│ ││ 原函数 │ │ │
│ │└────────│────────┘ │
│ └─────────┘ │
├─────────────────────────────────────────────┤
│ 生成器 = 工厂流水线 │
│ │
│ 原料 → [加工①] → 产出① → 暂停 │
│ [加工②] → 产出② → 暂停 │
│ [加工③] → 产出③ → 暂停 │
│ ... │
│ 不是一次性生产所有产品,而是按需生产 │
├─────────────────────────────────────────────┤
│ 上下文管理器 = 自动门 │
│ │
│ 进门(enter) → 你在里面做事 → 出门(exit) │
│ 不管你做没做完、有没有摔倒,门都会自动关 │
└─────────────────────────────────────────────┘
七、学习路径建议
| 阶段 | 目标 | 练习 |
|---|---|---|
| 入门 | 能看懂别人的装饰器/生成器/with | 阅读标准库源码(如functools.lru_cache) |
| 进阶 | 能写简单的装饰器和生成器 | 写一个@retry装饰器、写一个斐波那契生成器 |
| 熟练 | 能灵活组合三者 | 写一个带缓存的、可计时的、逐行处理文件的工具 |
| 精通 | 理解底层原理 | 研究Python的__call__、gi_frame、__exit__的异常处理机制 |
八、结语
这三个"魔法"机制,本质上都是Python对关注点分离的实践:
- 装饰器把"做什么"和"怎么做之前/之后"分离
- 生成器把"数据生产"和"数据消费"分离
- 上下文管理器把"资源获取"和"资源释放"分离
理解了这个本质,你就不会再觉得它们是"魔法",而是顺理成章的工具。
最好的学习方式就是动手写。 把这篇文章里的每个例子都敲一遍,改一改,跑一跑。当你能自然地用 @timer 给函数计时、用 yield 处理数据流、用 with 管理资源时,你就已经迈入了Python进阶的大门。