with语句为什么能自动关闭文件——上下文管理器
本文目录
先说结论
上下文管理器通过__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 Session | SparkSession | Session未关闭,集群资源不释放 |
| 临时文件 | tempfile.NamedTemporaryFile | 临时文件未删除占满磁盘 |
| 锁/信号量 | threading.Lock | 死锁 |
| 网络连接 | requests.Session | TCP连接泄漏 |
# 大数据开发中常见的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年大数据老兵,只讲大数据面试干货