• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Python面向对象:上下文管理器

Python编程 彭东稳 7年前 (2018-02-06) 22308次浏览 已收录 0个评论

一、上下文管理器

在使用Python编程中,可以会经常碰到这种情况:有一个特殊的语句块,在执行这个语句块之前需要先执行一些准备动作;当语句块执行完成后,需要继续执行一些收尾动作。

例如:当需要操作文件或数据库的时候,首先需要获取文件句柄或者数据库连接对象,当执行完相应的操作后,需要执行释放文件句柄或者关闭数据库连接的动作。又如,当多线程程序需要访问临界资源的时候,线程首先需要获取互斥锁,当执行完成并准备退出临界区的时候,需要释放互斥锁。

对于这些情况,Python中提供了上下文管理器(Context Manager)的概念,可以通过上下文管理器来定义/控制代码块执行前的准备动作,以及执行后的收尾动作。

那么在Python中怎么实现一个上下文管理器呢?Python的上下文管理器通常需要和三个概念结合起来:关键字with,两个魔法方法__enter____exit__。Python中的with语句和上下文管理器,是从2.5版本开始加入到Python语法中的。

也就是说,当我们需要创建一个上下文管理器类型的时候,就需要实现__enter____exit__方法,这对方法就称为上下文管理协议(Context Manager Protocol),定义了一种运行时上下文环境。下面就是关于这两个方法的具体介绍。

__enter__(self)

定义上下文管理器在with语句创建的块的开始处应该执行的操作。请注意,__enter__的返回值绑定到with语句的目标或as之后的变量名称。

__exit__(self, exception_type, exception_value, traceback)

定义上下文管理器在块被执行(或终止)之后应该执行的操作。它可以用于处理异常,执行清理,或者在块中的操作之后立即执行某些操作。如果该块成功执行,则exception_type,exception_value和traceback将为None。否则,你可以选择处理异常或让用户处理它;如果你想处理它,确保 __exit__ 完成之后返回True。如果你不想让这个异常被上下文管理器处理,就让它发生即可。

二、with语句

在Python中,可以通过with语句来方便的使用上下文管理器,with语句可以在代码块运行前进入一个运行时上下文(执行__enter__方法),并在代码块结束后退出该上下文(执行__exit__方法)。

with语句的语法如下:

这里就是一个标准的上下文管理器的使用逻辑,稍微解释一下其中的运行逻辑:

1)执行EXPR语句,获取上下文管理器(Context Manager)。

2)调用上下文管理器中的__enter__方法,该方法执行一些预处理工作。

3)这里的as VAR可以省略,如果不省略,则将__enter__方法的返回值赋值给VAR。

4)执行代码块BLOCK,这里的VAR可以当做普通变量使用。

5)最后调用上下文管理器中的的__exit__方法。

6)__exit__方法有三个参数:exception_type,exception_value,traceback。如果代码块BLOCK发生异常并退出,那么分别对应异常的type、value 和 traceback。否则三个参数全为None。

7)__exit__方法的返回值可以为True或者False。如果为True,那么表示异常被忽视,相当于进行了try-except操作;如果为False,则该异常会被重新raise。

在Python的内置类型中,很多类型都是支持上下文管理协议的,例如file,thread.LockType,threading.Lock等等。这里我们就以file类型为例,看看with语句的使用。

当需要写一个文件的时候,一般都会通过下面的方式。代码中使用了try-finally语句块,即使出现异常,也能保证关闭文件句柄。

Python的内置file类型是支持上下文管理协议的,可以直接通过内建函数 dir() 来查看file支持的方法和属性:

所以,可以通过 with 语句来简化上面的代码,代码的效果是一样的,但是使用 with 语句的代码更加的简洁:

三、自定义上下文管理器

对于自定义的类型,可以通过实现__enter____exit__方法来实现上下文管理器。看下面的代码,自己实现打开文件操作。

__exit__返回True,是说,这个异常已经被(以某种方法)处理了,别人不用关心这个异常了,其实这里也并没有处理。

使用实例:

代码很简单,也很容易理解,这里不做过多解释。

看下面的代码,代码中定义了一个MyTimer类型,这个上下文管理器可以实现代码块的计时功能:

下面结合with语句使用这个上下文管理器:

四、异常处理

在使用上下文管理器中,如果代码块产生了异常,__exit__方法将被调用,而__exit__方法又会有不同的异常处理方式。当__exit__方法退出当前运行的上下文时,会并返回一个布尔值,该布尔值表明了”如果代码块执行中产生了异常,该异常是否须要被忽略”。

1. __exit__返回False,重新抛出(re-raised)异常到上层

修改前面的例子,在MyTimer类型中加入了一个参数”ignoreException”来表示上下文管理器是否会忽略代码块中产生的异常。

使用try…except…else处理上下文管理器:

运行这段代码,会得到以下结果,由于__exit__方法返回False,所以代码块 (with_suite)中的异常会被继续抛到上层代码。

2. __exit__返回Ture,代码块中的异常被忽略

将代码改为__exit__返回为True的情况:

运行结果就变成下面的情况,代码块中的异常被忽略了,代码继续运行:

一定要小心使用__exit__返回Ture的情况,除非很清楚为什么这么做。

3. 通过__exit__函数完整的签名获取更多异常信息

对于__exit__函数,它的完整签名如下,也就是说通过这个函数可以获得更多异常相关的信息。

继续修改上面例子中的__exit__函数如下:

这次运行结果中,就显示出了更多异常相关的信息了:

五、内置库contextlib的使用

编写__enter____exit__仍然很繁琐,因此Python的标准库contextlib提供了更简单的写法,使得上线文管理器更加容易使用。其中包含如下功能:

1)@contextmanager

装饰器contextmanager,该装饰器将一个函数中yield语句之前的代码当做__enter__方法执行,yield语句之后的代码当做__exit__方法执行。同时yield返回值赋值给as后的变量。

@contextmanager这个decorator接受一个generator,用yield语句把with … as var把变量输出出去,然后,with语句就可以正常地工作了。

很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现。例如:

上述代码执行结果为:

代码的执行顺序是:

  1. with语句首先执行yield之前的语句,因此打印出<h1>;
  2. yield调用会执行with语句内部的所有语句,因此打印出hello和world;
  3. 最后执行yield之后的语句,打印出</h1>。

因此,@contextmanager让我们通过编写generator来简化上下文管理。

2)@closing

如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。例如,用with语句使用urlopen():

closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:

它的作用就是把任意对象变为上下文对象,并支持with语句。@contextlib还有一些其他decorator,便于我们编写更简洁的代码。

<参考>

contextlib

Python上下文管理器


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!