Python装饰器、生成器、上下文管理器:3个必会的"魔法"机制

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 异常

关键理解:

  1. 调用 counter() 时,函数体并没有执行
  2. 第一次 next(c),执行到第一个 yield,返回1,暂停
  3. 第二次 next(c),从暂停处继续,执行到第二个 yield,返回2,暂停
  4. 以此类推,直到没有更多 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 自己写一个上下文管理器

方法一:类实现(enterexit

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进阶的大门。

相关推荐
Go_error24 分钟前
Fyne:让 Go 开发者也能玩转 GUI
后端·go
用户07956750431428 分钟前
Python + OpenCV 识别 GIF 中旋转最快的图形:基于时间周期的动态识别
后端
长大198830 分钟前
Python+Flask 1小时搭建个人博客:比WordPress轻量10倍
后端
闪学it37 分钟前
Python测试开发进阶线上班28期
后端
Go_error37 分钟前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error41 分钟前
Go-redis:执行 Lua 脚本
后端·go
IT毕设实战小研1 小时前
电影数据可视化推荐系统
大数据·后端·爬虫·python·算法·信息可视化·课程设计
ssshooter1 小时前
AI 时代你不能不知道的 git worktree
前端·后端·面试
Cosolar2 小时前
一文弄懂 Agent Harness 与 Agent Runtime 的区别
java·后端·github