阅读本文需要一定的python使用基础
你有木有思考过,with语句背后的实现逻辑?为什么它会帮我们处理一些逻辑?上下文管理器又是什么?
本篇文章我们一起了解一下 with 运行原理,以及Python的 上下文管理器 。
我们先以一个具体的实例进行演示,比如打开一个文件,读取文件的内容
# 打开文件
f = open("test.txt", "r")
for info in f.read():
# do something
f.close()
很简单的例子,打开一个文件,读取文件内容进行操作,然后关闭资源。
但是,如果我们进行文件内容操作时,出现了异常,导致文件句柄无法释放,进而就会导致资源的泄露。
那如何避免这个问题?
我们可以使用try … finally …来优化
# 打开文件
f = open("test.txt", "r")
try:
for info in f.read():
# do something
finally:
f.close()
这样我们就能保证文件在操作过程中,无论是否发生异常,都能正确关闭资源。
但是这么优化会导致代码很繁琐,可读性变得很差。
针对这种情况我们就可以使用 with 语法块来解决这个问题。
# 打开文件
with open("test.txt", "r") as f:
for info in f.read():
# do something
用with语法块进行优化的代码,实现之前相同的功能,而且可读性也很好。
那么with语句究竟怎么运行呢?
首先,看一下__with__的语法格式。
with context_expression [as target(s)]:
# do something
可以看到,语法非常简单,只需要一个 with 表达式,然后执行自定义的业务逻辑。
理清几个概念
- 上下文表达式: with context_expression [as target(s)] , 如处理文件对象 with open(“test.txt”, “r”) as f
- 上下文管理器: context_expression 如处理文件对象 open(“test.txt”, “r”)
- f 不是上下文管理器,而是资源对象
但是,with 后面的表达式, 也就是 __context_expression__是随意写的吗?
答案是否定的。with 后面的语法对象需要实现 上下文管理器协议 。
在Python中,一个类方法,如果实现了以下方法,就实现了 上下文管理器协议 。
为了便于理解,我们实现一段代码,用Python记录一段代码所花费时间。
示例1
# 比如我们要统计一个列表,加入10000个数据,需要的时间
import time # 导入时间模块
start = int(time.time()) # 开始时间
nums = []
for i in range(10000):
nums.append(i)
end = int(time.time()) # 结束时间
print(f"cost time: {end - start} seconds.")
这样就实现了我们想要的功能;但是,我想换种方式,使用 上下文管理器 该怎么做呢?
分三步走:
- 先定义一个类方法
- 实现 __enter__ 和 __exit__方法
- 使用with语法调用
请看具体代码:
示例2
import time
class Timer: # 定义类
def __init__(self):
self.elapsed = 0
def __enter__(self): # 实现enter方法
self.start = int(time.time())
return self # 返回对象
def __exit__(self, exc_type, exc_val, exc_tb): # 实现exit方法
self.end = int(time.time())
self.elapsed = self.end - self.start
return False # 与enter保持统一
with Timer() as timer: # with调用
nums = []
for i in range(10000):
nums.append(i)
print(f"cost time : {timer.elapsed} seconds") # 打印时间
这样我们的需求就满足了。在这个例子中,我们实现了 Timer 类,它分别实现了 __enter__ 和 ____exit__ __方法。
具体解释一下执行流程:
__enter__ 在进行 with 语句前被调用,这个方法的返回值赋给了 with 之后的 timer变量
执行自定义代码
__exit__ 在业务代码块执行完后被调用
此外,如果__with__语句块内发生了异常,那么____exit____ 方法是可以拿到关于异常的信息的,可以选择是否打印出来,本示例未打印。
exc_type: 异常类型
exc_val:异常对象
exc_tb:异常堆栈信息
但是,不得不问了,这样写岂不是代码量增加了,本来我只需三行代码就完成了,为啥要写这么多?
我的回答是:
- 在简单的脚本测试中,示例1可以满足需求,完全没必要写这么复杂
- 在实际的项目开发,我认为示例2更好:
- 可以以一种的优雅的方式,处理资源(比如文件操作)
- 可以把部分处理逻辑写入这块代码,增加代码的可读性
- 可以处理异常
此时,不得不问了,是不是有更简单的实现上下文管理器的方式?因为有的时候,实在没必要因为一个小功能写这么复杂的代码。
答案是肯定的。Python提供了 __contextlib__模块,使用这个模块可以把__上下文管理器__当做__装饰器__使用。不过在本文中,不再讲述,感兴趣的看官可以自行搜索了解。
总结一下,使用上下文管理器有三个好处:
- 提高代码的复用率;
- 提高代码的优雅度;
- 提高代码的可读性;
主要应用场景:
- 资源的开关
- 资源的加锁,解锁
- 资源的改变、重置