订阅更新

with语句为什么能自动关闭文件——上下文管理器

Python手册 第 10 题 约 8 分钟读完

本文目录
  1. 先说结论
  2. with语句的执行流程
  3. 自定义上下文管理器——类实现
  4. 自定义上下文管理器——@contextmanager装饰器(更简洁)
  5. 大数据场景的上下文管理器应用
  6. 实战踩坑:数据库连接泄漏
  7. 面试高频追问
  8. 总结

先说结论

上下文管理器通过__enter__和__exit__两个魔术方法管理资源的获取和释放。with语句进入时调用__enter__(获取资源),退出时调用__exit__(释放资源),即使with块内抛异常也会调__exit__。这保证了资源一定会被释放。自定义上下文管理器可以用类实现(__enter__/__exit__),也可以用@contextmanager装饰器实现。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

with语句的执行流程

# with语句的等价代码
 
# 写法
with open('data.csv') as f:
    data = f.read()
    process(data)
 
# 等价于
f = open('data.csv')
try:
    data = f.read()
    process(data)
finally:
    f.close()  # 即使process()抛异常,也会执行close()
 
# with的好处:
# 1. 不会忘记关闭资源(finally自动执行)
# 2. 代码更简洁
# 3. 异常安全——即使with块内抛异常,资源也会释放

自定义上下文管理器——类实现

# 自定义数据库连接的上下文管理器
class DatabaseConnection:
    def __init__(self, connection_string):
        self.connection_string = connection_string
        self.conn = None
 
    def __enter__(self):
        self.conn = create_connection(self.connection_string)
        return self.conn  # with ... as conn 的值
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.conn:
            self.conn.close()  # 一定会关闭连接
            print('Connection closed')
        return False  # False=不吞异常,让异常继续传播
        # True=吞掉异常(不建议)
 
# 使用
with DatabaseConnection('mysql://host:3306/db') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM table')
    # with块结束后conn.close()自动执行

自定义上下文管理器——@contextmanager装饰器(更简洁)

from contextlib import contextmanager
 
@contextmanager
def timer(task_name):
    import time
    start = time.time()
    print(f'[{task_name}] Starting...')
    yield  # with块的代码在这里执行
    elapsed = time.time() - start
    print(f'[{task_name}] Done in {elapsed:.2f}s')
 
# 使用
with timer('ETL Processing'):
    process_data()  # yield之前的代码=__enter__
                    # yield之后的代码=__exit__
 
# yield之前的代码在进入with时执行(相当于__enter__)
# yield之后的代码在退出with时执行(相当于__exit__)
# 比类实现简洁得多,大部分场景推荐用@contextmanager

大数据场景的上下文管理器应用

场景资源不用with的风险
文件读写文件句柄文件句柄泄漏,系统打开文件数超限
数据库连接Connection/Cursor连接池耗尽,其他脚本无法连接
Spark SessionSparkSessionSession未关闭,集群资源不释放
临时文件tempfile.NamedTemporaryFile临时文件未删除占满磁盘
锁/信号量threading.Lock死锁
网络连接requests.SessionTCP连接泄漏
# 大数据开发中常见的with用法
 
# Spark Session
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
# Spark不原生支持with,但可以包装
 
# 数据库连接(最常见的资源泄漏场景)
import pymysql
with pymysql.connect(host='localhost', db='mydb') as conn:
    with conn.cursor() as cursor:
        cursor.execute('SELECT COUNT(*) FROM events')
        count = cursor.fetchone()[0]
# conn和cursor都会自动关闭
 
# 临时文件
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=True) as tmp:
    tmp.write('id,name\n1,Wood\n')
    tmp.flush()
    upload_to_s3(tmp.name)  # 上传
# with结束后临时文件自动删除

实战踩坑:数据库连接泄漏

在某短剧出海公司,有一个ETL脚本每天从MySQL读取广告投放数据。脚本里创建了数据库连接但没有显式关闭:

# 错误写法:连接没关闭
def fetch_ad_data(date):
    conn = pymysql.connect(host='db-host', db='ads')
    cursor = conn.cursor()
    cursor.execute(f"SELECT * FROM campaigns WHERE date='{date}'")
    data = cursor.fetchall()
    return data  # 连接没关!
 
# 每次调用都泄漏一个连接
# 数据库连接池默认100个连接
# 脚本跑100次后连接池耗尽
# 其他脚本报:'Too many connections'
 
# 正确写法:用with自动关闭
def fetch_ad_data(date):
    with pymysql.connect(host='db-host', db='ads') as conn:
        with conn.cursor() as cursor:
            cursor.execute(f"SELECT * FROM campaigns WHERE date='{date}'")
            return cursor.fetchall()
    # with结束后conn和cursor都自动关闭

教训:任何获取外部资源的操作(数据库/文件/网络)都应该用with语句。忘记关闭资源在开发环境可能没问题(脚本跑完进程退出自动释放),但在生产环境的长时间运行脚本(Airflow DAG、服务进程)中一定会出问题。

面试高频追问

追问一:__exit__的三个参数是什么?

exc_type(异常类型)、exc_val(异常值)、exc_tb(异常traceback)。如果with块内没有异常,三个参数都是None。如果有异常,三个参数包含异常信息。__exit__返回True表示吞掉异常(不向外传播),返回False或None表示让异常继续传播。通常不要返回True——异常应该让调用方知道。

追问二:一个with语句能管理多个资源吗?

可以。Python 3.1+支持:with open('a.csv') as f1, open('b.csv') as f2:。Python 3.10+支持用括号换行:with (open('a') as f1, open('b') as f2):更清晰。退出时按逆序关闭(先关f2再关f1)。

追问三:contextlib.suppress()是什么?

suppress()是一个上下文管理器,在with块内忽略指定的异常类型。with contextlib.suppress(FileNotFoundError): os.remove('temp.csv')如果文件不存在就静默忽略。比try-except-pass更简洁。在清理临时文件时很常用。

总结

上下文管理器面试讲清楚三层:① 原理(__enter__获取资源、__exit__释放资源、即使异常也会释放);② 两种实现方式(类实现__enter__/__exit__、@contextmanager装饰器更简洁);③ 大数据场景(文件/数据库连接/临时文件/锁都应该用with管理,避免资源泄漏)。能讲出数据库连接泄漏导致连接池耗尽的踩坑经历和改with后的修复方案,面试官会觉得你有生产环境的资源管理意识。

下一篇聊Python并发编程:多线程为什么没有加速CPU密集任务?threading/multiprocessing/asyncio怎么选?

你们ETL脚本用with管理资源了吗?遇到过资源泄漏吗?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,只讲大数据面试干货

Python 面试通关手册

第11-15篇:并发编程与工程规范

Wood聊大数据

9年大数据老兵,只讲大数据面试干货

刷题进度保存在当前浏览器