有人认为Python的with
语句是一个晦涩的特性,但只要你了解了其背后的原理,就不会感到神秘了。with
语句实际上是非常有用的特性,有助于编写更清晰易读的Python代码。
with
语句究竟有哪些好处?它有助于简化一些通用资源管理模式,抽象出其中的功能,将其分解并重用。
若想充分地使用这个特性,比较好的办法是查看Python标准库中的示例。内置的open()
函数就是一个很好的用例:
with open('hello.txt', 'w') as f:
f.write('hello, world!')
打开文件时一般建议使用with
语句,因为这样能确保打开的文件描述符在程序执行离开with
语句的上下文后自动关闭。本质上来说,上面的代码示例可转换成下面这样:
f = open('hello.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()
很明显,这段代码比with
语句冗长。注意,当中的try...finally
语句也很重要,只关注其中的逻辑代码还不够:
f = open('hello.txt', 'w')
f.write('hello, world')
f.close()
如果在调用f.write()
时发生异常,这段代码不能保证文件最后被关闭,因此程序可能会泄露文件描述符。此时with
语句就派上用场了,它能够简化资源的获取和释放。
threading.Lock
类是Python标准库中另一个比较好的示例,它有效地使用了with
语句:
some_lock = threading.Lock()
# 有问题:
some_lock.acquire()
try:
# 执行某些操作……
finally:
some_lock.release()
# 改进版:
with some_lock:
# 执行某些操作……
在这两个例子中,使用with
语句都可以抽象出大部分资源处理逻辑。不必每次都显式地写一个try...finally
语句,with
语句会自行处理。
with
语句不仅让处理系统资源的代码更易读,而且由于绝对不会忘记清理或释放资源,因此还可以避免bug或资源泄漏。
Python with语句 在自定义对象中支持with
无论是open()
函数和threading.Lock
类本身,还是它们与with
语句一起使用,这些都没有什么特殊之处。只要实现所谓的上下文管理器(context manager),就可以在自定义的类和函数中获得相同的功能。
详见Python文档: “With Statement Context Managers”。
上下文管理器是什么?这是一个简单的“协议”(或接口),自定义对象需要遵循这个接口来支持with
语句。总的来说,如果想将一个对象作为上下文管理器,需要做的就是向其中添加__enter__
和__exit__
方法。
Python将在资源管理周期的适当时间调用这两种方法。
来看看实际代码,下面是open()
上下文管理器的一个简单实现:
class ManagedFile:
def __init__(self, name):
self.name = name
def __enter__(self):
self.file = open(self.name, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
其中的ManagedFile
类遵循上下文管理器协议,所以与原来的open()
例子一样,也支持with
语句:
>>> with ManagedFile('hello.txt') as f:
... f.write('hello, world!')
... f.write('bye now')
当执行流程进入with
语句上下文时,Python会调用__enter__
获取资源;离开with
上下文时,Python会调用__exit__
释放资源。
在Python中,除了编写基于类的上下文管理器来支持with
语句以外,标准库中的contextlib
模块在上下文管理器基本协议的基础上提供了更多抽象。如果你遇到的情形正好能用到contextlib
提供的功能,那么可以节省很多精力。
详见Python文档:“contextlib”。
例如,使用contextlib.contextmanager
装饰器能够为资源定义一个基于生成器的工厂函数,该函数将自动支持with
语句。下面的示例用这种技术重写了之前的ManagedFile
上下文管理器:
from contextlib import contextmanager
@contextmanager
def managed_file(name):
try:
f = open(name, 'w')
yield f
finally:
f.close()
>>> with managed_file('hello.txt') as f:
... f.write('hello, world!')
... f.write('bye now')
这个managed_file()
是生成器,开始先获取资源,之后暂停执行并产生资源以供调用者使用。当调用者离开with
上下文时,生成器继续执行剩余的清理步骤,并将资源释放回系统。
基于类的实现和基于生成器的实现基本上是等价的,选择哪一种取决于你的编码偏好。
基于@contextmanager
的实现有一个缺点,即这种方式需要对装饰器和生成器等Python高级概念有所了解。
再次提醒,选择哪种实现取决于你自己和团队中其他人的编码偏好。
Python with语句 用上下文管理器编写漂亮的API
上下文管理器非常灵活,巧妙地使用with
语句能够为模块和类定义方便的API。
例如,如果想要管理的“资源”是某种报告生成程序中的文本缩进层次,可以编写下面这样的代码:
with Indenter() as indent:
indent.print('hi!')
with indent:
indent.print('hello')
with indent:
indent.print('bonjour')
indent.print('hey')
这些语句读起来有点像用于缩进文本的领域特定语言(DSL)。注意这段代码多次进入并离开相同的文本管理器,以此来更改缩进级别。运行这段代码会在命令行中整齐地显示出下面的内容:
hi!
hello
bonjour
hey
那么如何实现一个上下文管理器来支持这种功能呢?
顺便说一句,这是一个不错的练习,从中可以准确理解上下文管理器的工作方式。因此在查看下面的实现之前,最好先花一些时间尝试自行实现。
如果你已经准备好查看我的实现,那么下面就是使用基于类的上下文管理器来实现的方法:
class Indenter:
def __init__(self):
self.level = 0
def __enter__(self):
self.level += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.level -= 1
def print(self, text):
print(' ' * self.level + text)
还不错,是吧?希望你现在能熟练地在自己的Python程序中使用上下文管理器和with
语句了。这两个功能很不错,可以用来以更加有Python特色和可维护的方式处理资源管理问题。
如果你还想再找一个练习来加深理解,可以尝试实现一个使用time.time
函数来测量代码块执行时间的上下文管理器。一定要试着分别编写基于装饰器和基于类的变体,以此来彻底弄清楚两者的区别。
Python with语句 关键要点
-
with
语句通过在所谓的上下文管理器中封装try...finally
语句的标准用法来简化异常处理。 -
with
语句一般用来管理系统资源的安全获取和释放。资源首先由with
语句获取,并在执行离开with
上下文时自动释放。 -
有效地使用
with
有助于避免资源泄漏的问题,让代码更加易于阅读。