Python里with语句的用法与技巧


新手们刚开始学习Python时,会用以下方法读取文件:

fp=open(b'file_name','r')
try:
    data=fp.read()
except:
    pass
finally:
    fp.close()

python可是以简洁著称,这么一大串有损python美名;

with语句是从python2.5开始引入的一种与异常处理相关的功能(2.5 版本中要通过 from __future__ import with_statement 导入后才可以使用),从2.6版本后可直接使用。 with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。

使用with语句重写代码:

with open(b'file_name','r') as fp:
    data=fp.read()

修改后,顿时养眼了许多,实际上这段代码是执行了fp.close()的;如果想将这种技巧运用到自定义的函数中,应该怎么实现呢?

先要来了解with是怎么实现的;

我们要先明白上下文管理器这个概念;上下文管理协议:包含方法__enter__()和__exit__(),我们要使用with就要让对象实现这两个方法。

现在我们来自定义一个类来使用with语句:

class DummyResource:
def __init__(self, tag):
        self.tag = tag
        print 'Resource [%s]' % tag
    def __enter__(self):
        print '[Enter %s]: Allocate resource.' % self.tag
        return self   # 可以返回不同的对象
    def __exit__(self, exc_type, exc_value, exc_tb):
        print '[Exit %s]: Free resource.' % self.tag
        if exc_tb is None:
            print '[Exit %s]: Exited without exception.' % self.tag
        else:
            print '[Exit %s]: Exited with exception raised.' % self.tag
            return False   # 可以省略,缺省的None也是被看做是False

DummyResource 中的 __enter__() 返回的是自身的引用,这个引用可以赋值给 as 子句中的 target 变量;返回值的类型可以根据实际需要设置为不同的类型,不必是上下文管理器对象本身。

__exit__() 方法中对变量 exc_tb 进行检测,如果不为 None,表示发生了异常,返回 False 表示需要由外部代码逻辑对异常进行处理;注意到如果没有发生异常,缺省的返回值为 None,在布尔环境中也是被看做 False,但是由于没有异常发生,__exit__() 的三个参数都为 None,上下文管理代码可以检测这种情况,做正常处理。

下面在 with 语句中访问 DummyResource :

with DummyResource('Normal'):
    print '[with-body] Run without exceptions.'
with DummyResource('With-Exception'):
    print '[with-body] Run with exception.'
    raise Exception
    print '[with-body] Run with exception. Failed to finish statement-body!'

第一个with语句执行结果如下:

Resource [Normal]
[Enter Normal]: Allocate resource.
[with-body] Run without exceptions.
[Exit Normal]: Free resource.
[Exit Normal]: Exited without exception.

可以看到,正常执行时会先执行完语句体 with-body,然后执行 __exit__() 方法释放资源。

第2个 with 语句的执行结果如下 :

Resource [With-Exception] [Enter With-Exception]: Allocate resource. [with-body] Run with exception. [Exit With-Exception]: Free resource. [Exit With-Exception]: Exited with exception raised.Traceback (most recent call last): File "G:/demo", line 20, in raise ExceptionException?

with语句的应用

线程锁中的应用,原始代码:

import threading
lock= threading.Lock()
...
lock.acquire()
pass
lock.release()

使用with语句后的线程锁:

import threading
lock=threading.Lock()
...
with lock:
    pass

contextlib模块

contextlib 模块提供了3个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。使用这些对象,可以对已有的生成器函数或者对象进行包装,加入对上下文管理协议的支持,避免了专门编写上下文管理器来支持 with 语句。

装饰器 contextmanager

contextmanager 用于对生成器函数进行装饰,生成器函数被装饰以后,返回的是一个上下文管理器,其 __enter__() 和 __exit__() 方法由 contextmanager 负责提供,而不再是之前的迭代子。被装饰的生成器函数只能产生一个值,否则会导致异常 RuntimeError;产生的值会赋值给 as 子句中的 target,如果使用了 as 子句的话。下面看一个简单的例子。

from contextlib import contextmanager
@contextmanager
def demo():
    print '[Allocate resources]'
    print 'Code before yield-statement executes in __enter__'
    yield '*** contextmanager demo ***'
    print 'Code after yield-statement executes in __exit__'
    print '[Free resources]'
with demo() as value:
    print 'Assigned Value: %s' % value

结果输出如下:

[Allocate resources]
Code before yield-statement executes in __enter__
Assigned Value: *** contextmanager demo ***
Code after yield-statement executes in __exit__
[Free resources]

可以看到,生成器函数中 yield 之前的语句在 __enter__() 方法中执行,yield 之后的语句在 __exit__() 中执行,而 yield 产生的值赋给了 as 子句中的 value 变量。

需要注意的是,contextmanager 只是省略了 __enter__() / __exit__() 的编写,但并不负责实现资源的“获取”和“清理”工作;“获取”操作需要定义在 yield 语句之前,“清理”操作需要定义 yield 语句之后,这样 with 语句在执行 __enter__() / __exit__() 方法时会执行这些语句以获取/释放资源,即生成器函数中需要实现必要的逻辑控制,包括资源访问出现错误时抛出适当的异常。

函数 nested

nested 可以将多个上下文管理器组织在一起,避免使用嵌套 with 语句。

with nested(A(), B(), C()) as (X, Y, Z):
     # with-body code here

类似于:

with A() as X:
    with B() as Y:
        with C() as Z:
             # with-body code here

需要注意的是,发生异常后,如果某个上下文管理器的 __exit__() 方法对异常处理返回 False,则更外层的上下文管理器不会监测到异常。

上下文管理器 closing

closing 的实现如下:

class closing(object):
    # help doc here
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

上下文管理器会将包装的对象赋值给 as 子句的 target 变量,同时保证打开的对象在 with-body 执行完后会关闭掉。closing 上下文管理器包装起来的对象必须提供 close() 方法的定义,否则执行时会报 AttributeError 错误。

class ClosingDemo(object):
    def __init__(self):
        self.acquire()
    def acquire(self):
        print 'Acquire resources.'
    def free(self):
        print 'Clean up any resources acquired.'
    def close(self):
        self.free()
with closing(ClosingDemo()):
    print 'Using resources'

结果输出如下:

Acquire resources.
Using resources
Clean up any resources acquired.

closing 适用于提供了 close() 实现的对象,比如网络连接、数据库连接等,也可以在自定义类时通过接口 close() 来执行所需要的资源“清理”工作。


如需转载,请注明出处,本文地址:https://www.perfcode.com/p/502.html