理解 Python 的 with 语句

翻译自 Understanding Python’s “with” statement,有改动。

从 comp.lang.python 以及其它论坛来看,Python 2.5 新引入的 with 语句对于有经验的 Python 编程人员来说,甚至都有点困惑。

其实正如 Python 中的其他事物一样,一旦你理解了 with 语句尝试解决的问题,就会发现其实它非常简单。考虑下面这段代码:

set things up
try:
    do something
finally:
    tear things down    

这里,”set things up” 部分可以是打开一个文件,或者获取某种外部资源;”tear things down” 部分则可以是关闭文件,释放或者移除资源。try-finally 结构保证即使是 try 内的代码没有正常完成执行,”tear things down” 部分总是可以执行。

如果你经常这么做,那么你可以把 “set things up” 和 “tear things down” 部分的代码放入一个库函数,从而更容易重用。你当然可以类似这么做:

def controlled_execution(callback):
    set things up
    try:
        callback(thing)
    finally:
        tear things down

def my_function(thing):
    do something

controlled_execution(my_function)

但是上述代码有点冗长,特别是你需要修改局部变量时。另一个方法是使用一个one-shot 生成器,然后使用 for-in 语句来“包装”(wrap)代码:

def controlled_execution():
    set things up
    try:
        yield thing
    finally:
        tear things down

for thing in controlled_execution():
    do something with thing

但是在 2.4 和更早版本中,yield 甚至不被允许出现在 try-finally 中。虽然这可以被修复(而且在 2.5 中这已经被修复了),但是当你知道你只想执行一次代码,使用循环结构就有点奇怪了。所以在考虑了一些替代方案后,GvR 和 python 开发团队最终想出了一个更通用的方法,即使用一个对象而不是一个生成器来控制外部代码的行为:

class controlled_execution:
        def __enter__(self):
            set things up
            return thing
        def __exit__(self, type, value, traceback):
            tear things down

with controlled_execution() as thing:
        some code

现在,当 “with” 语句被执行时, Python 将会对表达式求值,然后对结果值(又称为 “context guard”)调用 __enter__ 方法,接着将 __enter__ 返回的值赋值给 as 语句后给出的变量。随后,Python 将会执行代码主体(即 some code 部分);最后不论代码主体中发生了什么,都将会调用警卫对象(guard object)的 __exit__ 方法。

作为额外的奖励,__exit__ 方法可以查看异常(如果有的话),并且必要时可以压制(surpress)或者处理它。为了压制异常,只需要返回一个真值。如,以下__exit__ 方法可以忽略任何 TypeError,但是允许其他所有异常通过:

def __exit__(self, type, value, traceback):
     return isinstance(value, TypeError)

在 Python 2.5中,文本对象已经配备了 __enter__ 和 __exit__ 方法;前者只返回文本对象本身,而后者则关闭文本。

>>> f = open("x.txt")
>>> f
<_io.TextIOWrapper name='x.txt' mode='r' encoding='cp936'>
>>> f.__enter__()
<_io.TextIOWrapper name='x.txt' mode='r' encoding='cp936'>
>>> f.read(1)
'X'
>>> f.__exit__(None, None, None)
>>> f.read(1)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file

所以为了打开文件,处理其内容,并且确保能关闭它,你可以简单地这么做:

with open("x.txt") as f:
    data = f.read()
    do something with data

总结:

with 语句能自动调用对象的 __enter__ 方法,完成 “set things up” 部分;在结束操作后会自动调用 __exit__ 方法完成 “tear things down” 部分。 而文本对象已经定义了 __enter__ 和 __exit__ 方法,所以我们可以直接使用 with 语句对文本对象进行操作,而不用关心关闭文本对象等操作。

事实上,像文本对象这样有 __enter__ 和 __exit__ 方法又可以称为上下文管理器(context manager)。当然,我们可以自定义自己的上下文管理器:定义__enter__ 方法,完成 “set things up”;定义 __exit__ 方法,完成 “tear things down” 部分。然后就可以使用 with 语句调用了。

可以参考这篇博客中的例子,定义我们自己的上下文管理器来处理文件:

class File():

    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.open_file = open(self.filename, self.mode)
        return self.open_file

    def __exit__(self, *args):
        self.open_file.close()

files = []
for _ in range(10000):
    with File('foo.txt', 'w') as infile:
        infile.write('foo')
        files.append(infile)

 

Previous post: 理解 Python 的 for 语句

Next post: 理解 Python 的生成器

posted @ 2017-07-26 14:45  天涯海角路  阅读(165)  评论(0)    收藏  举报