Python 的 with 语句(1)

参考了 浅谈 Python 的 with 语句

最近组里在CoDe(Competence Development,能力培养)。花姐讲文件操作,例子里有如下的代码:

with open(r'demo.txt') as f:
    for line in f:
        print line
        # ...more code

语句很简单,就是把demo.txt文件打开,逐行打印出来。我感兴趣的是这里的with语句,这个用法很简洁,比下面的try … except 用法要少一些代码量。

f = open(r'demo.txt')
try:
    for line in f:
        print line
        # ...more code
finally:
    f.close()

可以看到,下面的这种用法代码稍微复杂一些,还涉及到了文件的关闭操作。而with语句是怎么避免了显性的关闭文件呢?更进一步,with这个语句是怎么实现的,它的目的和好处是什么呢?

对比着看,可以感觉到with做了一系列的操作,准备工作、用户代码、扫尾工作。准备工作就是打开文件并且返回文件句柄(是不是这么定义?),用户代码就是中间那一部分逐行打印。扫尾工作就是关闭文件。

再做一个类比。在用robotframework做自动化测试时,总会定义 suite setup 和 suite teardown,这就是保证测试的进入和退出都有一定的保护措施,进入时做准备,退出时打扫战场。Python 的 with 语句就是suite setup 和 suite teardown的作用,是Python 2.5以后引入的特性。

在Python 2.5里要通过

from __future__ import with_statement

才能使用with 语句,到了Python 2.6就可以直接用了,它从一个实验性的特性变成了内置的了。

直接了当说,with的原理是实现了一个上下文管理协议(Context Management Protocol),而这个协议的两个必要部分就是两个方法,__enter__() 和 __exit__(),任何支持该协议的对象要实现这两个方法。一目了然了吧,这两个方法可以对应robotframework里的suite setup 和 suite teardown

先看一个例子吧。

class DummyResource(object):
    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

这段代码我是从最顶上那个帖子里抄的。可以做一些说明。

  • 这是一个叫DummyResource的类,提供了__enter__() 和 __exit__()两个方法,说明这个类是可以用with语句来调用的。另外从类名我们可以感觉到,with语句的运用都是和资源相关的。什么是资源?文件,网络套接字,线程thread,等等这些都是资源。
  • __enter__()方法里,除了一句打印“山顶的朋友,我在这里”,就是一句返回语句:
class DummyResource(object):
    # ...
    def __enter__(self):
        print '[Enter %s]: Allocate resource.' % self.tag
        return self	
    # ...

返回了一个self!这是什么意思?就是把这个类的实例返回出去。这样

with context_expression [as target(s)]:
    with-body

中的as target(s)就可以得到这个值了。忘了说,target(s)之所以有或许复数,是因为__enter__()方法是可以返回多个值成为一个元组的。

  • __exit__() 是个更复杂的方法,用来定义退出时候的动作。在方法的定义中可以看到有额外的参数exc_type、exc_value 和 exc_tb。这三个都是和异常相关的。如果with语句包住的用户代码正常执行,那么这三个变量都是None;反之,如果出错了,可以简单通过exc_type来判断异常的类型,配合后面两个变量获得更多的异常信息,进一步做相应的处理。
  • __exit__()方法中更重要的是它的返回值。return False。False的意思是“我还没有处理完,我要把这个异常继续抛出去,你们外面的代码去抓住它继续处理吧”。如果心满意足的,或者心怀鬼胎的觉得已经处理完了,就返回个真值,外面的代码就不会知道有异常发生过。很多问题的根本原因就这么湮没了。:-(

结合这个类定义,看看实用的调用例子吧。
小乖乖:

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!'

小乖乖的输出是这样的:

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

很好,按部就班,只要读得懂类代码就很容易看出这些输出。下面看一下“小淘气”的输出呢。

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 <module>
   raise Exception
Exception

除了读代码能看清的输出,还要说一两句。

  • 用户代码中是先抛出了异常后又打印了一句“[with-body] Run with exception. Failed to finish statement-body!”。在输出中这句没有打印。这说明了__exit__()方法是清道夫,你们用户代码怎么乱糟糟,你先跑,抛出异常了,就别接着往下走了,我留下来打扫战场。
  • 输出中的“Traceback (most recent call last):”一段是解释器抛出的异常,在__exit__()方法中的开关就是最后的那个返回否值。如果是返回真值,这段Traceback是不会输出的。

最后,说说最初的那个花姐举出来的例子。

with open(r'demo.txt') as f:
    for line in f:
        print line
        # ...more code

打开一个Python的IDE(我用的是Pycharm CE),可以点到open的定义,就是一句话。

def open(name, mode=None, buffering=None): # real signature unknown; restored from __doc__
    """
    open(name[, mode[, buffering]]) -> file object
    
    Open a file using the file() type, returns a file object.  This is the
    preferred way to open a file.  See file.__doc__ for further information.
    """
    return file('/dev/null')

仅仅返回一个file实例。继续点过去,就豁然开朗了。

class file(object):
     # ... some codes

    def __enter__(self): # real signature unknown; restored from __doc__
        """ __enter__() -> self. """
        return self

    def __exit__(self, *excinfo): # real signature unknown; restored from __doc__
        """ __exit__(*excinfo) -> None.  Closes the file. """
        pass
     # ... more codes

file 这个类 实现了__enter__()和__exit__()方法,那外面当然可以用with语句来建立文件操作的上下文。

其实实现上下文管理协议并不是每次都需要写一个类实现两个办法,还有其他的方法,会另开一篇写写。

-EOF-

发表评论

电子邮件地址不会被公开。 必填项已用*标注