跳转至

199_Python上下文管理器

一句话概述

Python上下文管理器通过with语句和__enter__/__exit__协议(或@contextmanager装饰器)自动管理资源的获取和释放,确保文件、数据库连接、锁等资源在使用完毕后被正确清理,即使发生异常也不会泄漏。

核心知识点表格

知识点说明
with语句上下文管理器的语法入口
enter进入上下文时调用,返回值赋给as变量
exit退出上下文时调用,处理异常和清理
@contextmanager用生成器函数快速创建上下文管理器
contextlib模块提供多种上下文管理器工具
资源管理文件、锁、数据库连接、临时目录等
异常处理__exit__返回True可以抑制异常
异步上下文async with + aenter/aexit

步骤详解

第一步:基本上下文管理器

白话解释:上下文管理器就像一个"管家",你进门时它帮你开灯(enter),离开时帮你关灯(exit),即使你匆忙逃离(异常)也不会忘记关灯。

# 基于类的上下文管理器
class FileManager:
    def __init__(self, filename, mode='r'):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        # 返回False(默认):异常继续传播
        # 返回True:异常被抑制
        return False

with FileManager('data.txt', 'w') as f:
    f.write("Hello, Context Manager!")

# 数据库连接管理器
class DatabaseConnection:
    def __init__(self, connection_string):
        self.connection_string = connection_string
        self.conn = None

    def __enter__(self):
        import sqlite3
        self.conn = sqlite3.connect(self.connection_string)
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.conn.rollback()  # 异常时回滚
        else:
            self.conn.commit()    # 正常时提交
        self.conn.close()
        return False

with DatabaseConnection('mydb.sqlite') as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (name TEXT)")
    cursor.execute("INSERT INTO users VALUES ('Alice')")

第二步:使用@contextmanager装饰器

白话解释:写一个类太麻烦了。@contextmanager让你用生成器函数(带yield的函数)快速创建上下文管理器。yield之前是__enter__,yield之后是__exit__。

from contextlib import contextmanager
import time

@contextmanager
def timer(label=""):
    """计时上下文管理器"""
    start = time.perf_counter()
    try:
        yield  # 这里把控制权交给with块
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label} 耗时: {elapsed:.4f}s")

with timer("数据处理"):
    time.sleep(1)
    # 数据处理 耗时: 1.0001s

@contextmanager
def temporary_directory():
    """创建临时目录,用完自动删除"""
    import tempfile, shutil
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir
    finally:
        shutil.rmtree(tmpdir)

with temporary_directory() as tmpdir:
    print(f"临时目录: {tmpdir}")
    # 使用临时目录...
# 退出后自动删除

@contextmanager
def change_directory(path):
    """临时切换工作目录"""
    import os
    old_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old_dir)

@contextmanager
def suppress_output():
    """抑制stdout输出"""
    import sys, os
    old_stdout = sys.stdout
    sys.stdout = open(os.devnull, 'w')
    try:
        yield
    finally:
        sys.stdout.close()
        sys.stdout = old_stdout

第三步:高级模式

from contextlib import ExitStack, suppress, redirect_stdout

# 1. ExitStack:动态管理多个上下文
with ExitStack() as stack:
    files = [stack.enter_context(open(f)) for f in ['a.txt', 'b.txt', 'c.txt']]
    # 所有文件在退出时自动关闭

# 2. suppress:忽略特定异常
from contextlib import suppress
with suppress(FileNotFoundError):
    os.remove('nonexistent.txt')  # 不存在也不会报错

# 3. 可重入上下文管理器
@contextmanager
def managed_resource():
    print("获取资源")
    yield "resource"
    print("释放资源")

# 4. 上下文管理器作为装饰器
class log_execution:
    """同时作为上下文管理器和装饰器"""
    def __init__(self, func_or_label=None):
        if callable(func_or_label):
            self.label = func_or_label.__name__
            self.func = func_or_label
        else:
            self.label = func_or_label or "block"
            self.func = None

    def __enter__(self):
        print(f"[START] {self.label}")
        self.start = time.perf_counter()
        return self

    def __exit__(self, *exc):
        elapsed = time.perf_counter() - self.start
        print(f"[END] {self.label} ({elapsed:.4f}s)")
        return False

    def __call__(self, *args, **kwargs):
        if self.func:
            with self:
                return self.func(*args, **kwargs)

# 5. 异步上下文管理器
class AsyncDBConnection:
    async def __aenter__(self):
        self.conn = await asyncio.connect("db://localhost")
        return self.conn

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.conn.close()

# async with AsyncDBConnection() as conn:
#     await conn.execute("SELECT 1")

实战命令速查

# 文件操作(最常见)
with open('file.txt', 'r') as f:
    data = f.read()

# 锁管理
import threading
lock = threading.Lock()
with lock:
    # 临界区代码
    pass

# 多上下文管理(Python 3.10+)
with (open('a.txt') as a, open('b.txt') as b):
    pass

# contextlib实用工具
from contextlib import closing, suppress, redirect_stdout, ExitStack
with closing(urllib.request.urlopen('http://example.com')) as page:
    pass

面试常问点

Q1: __exit__方法的三个参数是什么?返回True和False有什么区别? A: exc_type是异常类型,exc_val是异常实例,exc_tb是traceback对象。如果没有异常发生,三个参数都是None。返回True表示异常已被处理(抑制),返回False(默认)表示异常继续向上传播。

Q2: @contextmanager和类方式实现各自的优劣? A: @contextmanager更简洁(只需yield),适合简单场景。类方式更灵活,支持继承和状态管理,适合复杂场景。类方式可以同时作为装饰器使用(实现__call__)。

Q3: with语句的执行流程是什么? A: (1)计算with后的表达式得到上下文管理器对象;(2)调用__enter__(),返回值赋给as变量;(3)执行with块中的代码;(4)无论是否发生异常,调用__exit__();(5)如果有异常且__exit__返回False,重新抛出异常。

Q4: ExitStack的使用场景是什么? A: 当需要管理的上下文数量在运行时才能确定时(如打开动态数量的文件)。ExitStack维护一个清理回调栈,按后进先出的顺序执行清理。

Q5: 上下文管理器与try/finally有什么关系? A: 上下文管理器是try/finally的抽象封装。with语句保证__exit__始终执行,等价于try/finally中的finally块。优势是代码更简洁、可复用、不易遗漏清理逻辑。

易错点

  1. @contextmanager中忘记try/finally:yield后的代码在异常时可能不执行
  2. __exit__中吞掉异常:错误地返回True导致异常被静默忽略
  3. yield多次:@contextmanager的生成器只能yield一次
  4. 忘记as关键字:with open('f') as f中的as不能省略(如果需要引用)
  5. 上下文管理器对象复用:某些上下文管理器不支持重复进入

补充知识

contextlib模块常用工具

工具用途
@contextmanager用生成器创建上下文管理器
closing(thing)确保调用thing.close()
suppress(*exceptions)忽略指定异常
redirect_stdout(new)重定向标准输出
ExitStack动态管理多个上下文
nullcontext(value)什么都不做的上下文管理器
@asynccontextmanager异步版@contextmanager