Python with语句

有人认为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有助于避免资源泄漏的问题,让代码更加易于阅读。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程