Python - with 语句
管理外部资源的背景
- 在编程中会面临的一个常见问题是如何正确管理外部资源,例如文件、锁和网络连接
- 有时,程序会永远保留这些资源,即使不再需要它们,这种现象称为内存泄漏
- 因为每次创建和打开给定资源的新实例而不关闭现有资源时,可用内存都会减少
如何正确管理资源
- 正确管理资源通常是一个棘手的问题
- 它需要一个设置阶段和一个清理阶段
- 后一个阶段需要执行一些清理操作,例如关闭文件、释放锁或关闭网络连接
- 如果忘记执行这些清理操作,那么应用程序将使资源保持活动状态,这可能会损害宝贵的系统资源,例如内存和网络带宽
数据库连接数问题
- 最常见的数据库连接数问题
- 使用数据库时,可能会出现程序不断创建新连接而不释放或重用它们
- 在这种情况下,数据库后端可以停止接受新连接
- 这可能需要管理员登录并手动终止那些陈旧的连接以使数据库再次可用
写入文件问题
- 将文本写入文件通常是一种缓冲操作
- 这意味着对文件调用 .write() 不会立即导致将文本写入物理文件,而是写入临时缓冲区
- 有时,当缓冲区未满而开发人员忘记调用 .close() 时,部分数据可能会永远丢失
with 的作用
常规说法
- with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源
- 比如文件使用后自动关闭/线程中锁的自动获取和释放等。
官方解释
- 仅适用于执行上下文管理器定义的方法的代码块
- 允许对普通的 try...except...finally 使用模式进行封装以方便地重用
一句话总结
使用 with as 语句操作上下文管理器(context manager),它能够帮助我们自动分配并且释放资源
什么是上下文管理器
with as 的基本语法
with 表达式 [as target]:
代码块
执行顺序
- 调用表达式以获取上下文管理器
- 存储上下文管理器的 .__enter__() 和 .__exit__() 方法供以后使用
- 在上下文管理器上调用 .__enter__() 并将其返回值绑定到 target(如果有的话)
- 执行 with 代码块
- 当 with 代码块完成时,在上下文管理器上调用 .__exit__()
访问文件的代码演进
最基础的写法
# 1、打开文件 file = open("1.txt") # 2、读取文件 data = file.read() # 3、手动关闭文件 file.close()
存在的问题
在第二步假设文件读取的时候发生异常,没有做任何处理,就不会执行第三步,导致程序可能会泄露文件描述符
使用 try...except...finally 优化
try: # 打开文件、读取文件 f = open('xxx') data = f.read() except Exception as e: # 捕获异常 pass finally: # 关闭文件 f.close()
- 无论是否抛出异常,最后还是会关闭文件,解决上面提到的问题
- 但新的问题在于,代码比较冗余,而且要手动关闭文件
使用 with 优化
with open("1.txt") as file: data = file.read()
- 作用和 try 写法一样
- 优势:代码简洁,自动关闭文件,释放资源
- with 代码块执行完后,会自动调用文件对象的 .close() 方法
支持多个上下文管理器
with open("input.txt") as in_file, open("output.txt", "w") as out_file: # 从 input.txt 读取内容 # 转换内容 # 将转换后的内容写入output.txt pass
等价写法
with open("input.txt") as in_file: with open("output.txt", "w") as out_file: pass
使用 pathlib.Path.open()
import pathlib file_path = pathlib.Path("a.txt") with file_path.open("w") as file: file.write("Hello, World!")
- 由于 pathlib 提供了一种优雅、直接和 Pythonic 的方式来操作文件系统路径
- 因此应该考虑在 with 语句中使用 Path.open() 作为 Python 中的最佳实践
捕获异常的栗子
无论何时加载外部文件的程序都应检查可能存在的问题,例如文件丢失、读写访问等
import pathlib import logging file_path = pathlib.Path("a.txt") try: with file_path.open("w") as file: file.write("Hello, World!") except OSError as error: logging.error("Writing to file %s failed due to: %s", file_path, error)
- 在 with as 外层添加 try ... except 用于捕获异常
- 如果在执行 with 期间发生 OSError,则使用日志记录错误信息
遍历目录的栗子
import os with os.scandir(".") as entries: for entry in entries: print(entry.name, "->", entry.stat().st_size, "bytes")
- scandir() 会返回一个支持上下文管理协议的迭代器
- .__exit__() 将调用 scandir.close() 关闭迭代器并释放获取的资源
输出结果
__init__.py -> 178 bytes a.txt -> 13 bytes 1_上下文管理器.py -> 2168 bytes
高精度计算
# 高精度计算 from decimal import Decimal, localcontext with localcontext() as ctx: ctx.prec = 42 res = Decimal("1") / Decimal("42") print(res)
输出结果
0.0238095238095238095238095238095238095238095
扩展阅读
https://realpython.com/python-with-statement/