13.上下文管理
六、上下文管理
上下文管理可以包装任意格式的代码块
上下文管理的语法
上下文管理器是一个包装任意代码块的对象。上下文管理器保证进入上下文管理器时,每次代码执行的一致性;当退出上下文管理器时,
相关的资源会被正确回收。
值得注意的是,上下文管理器一定能够保证退出步骤的执行。如果进入上下文管理器,根据定义,一定会有退出步骤。即使内部代码抛出了异常,这点也成立。事实上,如果退出步骤处理异常合适,那么上下文管理器的退出代码为处理这类异常提供了一个机会(虽然不强制要求)。
因此上下文管理器的功能类似于执行try、except和finally关键字。通常,这也是一种封装需要被重复使用的try-except-finally结构的有效机制。
上下文管理器的任务是:代码块执行前准备,代码块执行后收拾
with子句
1、当 with 语句执行时,便执行上下文符号(译者注:就是 with 与 as 间内容)来获得一个上下文管理器.
2、上下文管理器的职责是提供一个上下文对象.这是通过调用__context__()方法来实现的 。该方法返回一个上下文对象,用于在 with 语句块中处理细节
3、需要注意的是上下文对象本身就可以是上下文管理器.所以 context_expr 既可以为一个真正的上下文管理器,也可以是一个可以自我管理的上下文对象
4、一旦我们获得了上下文对象,就会调用它的__enter()__方法
5、在将打开文件的操作放在with语句中,代码块结束后,文件将自动关闭
6、上下文管理器比文件自身多了一个异常处理功能,它允许我们把文件处理代码包装到一个逻辑层中,以确保在退出后可以自动关闭文件,
而不是依赖于垃圾收集上的自动关闭
7、with 语法的基本用法看上去如下:
with context_expr [as var]:
with_suite
例子:
>>> with open('foo.py') as f:
... data = f.readlines()
...
>>> f.closed
True
8、它仅能工作于支持上下文管理协议(context management protocol)的对象.这显然意味着只有内建了"上下文管理"的对象可以和 with 一起工作
如何使用上下文管理器:
如何打开一个文件,并写入"hello world"
filename="my.txt"
mode="w"
f=open(filename,mode)
f.write("hello world")
f.close()
当发生异常时(如磁盘写满),就没有机会执行第5行。当然,我们可以采用try-finally语句块进行包装:
writer=open(filename,mode)
try:
writer.write("hello world")
finally:
writer.close()
当我们进行复杂的操作时,try-finally语句就会变得丑陋,采用with语句重写:
with open(filename,mode) as writer:
writer.write("hello world")
as指代了从open()函数返回的内容,并把它赋给了新值。with完成了try-finally的任务。
自定义上下文管理器
with语句的作用类似于try-finally,提供一种上下文机制。要应用with语句的类,其内部必须提供两个内置函数__enter__和__exit__。前者在主体代码执行前执行,后者在主体代码执行后执行。as后面的变量,是在__enter__函数中返回的。
#!/usr/bin/env python #coding:utf8 class echo(): def output(self): print "hello world" def __enter__(self): print "enter" return self #可以返回任何希望返回的东西 def __exit__(self,exception_type,value,trackback): print "exit" if exception_type==ValueError: return True else: return False echo = echo() with echo as e: e.output()
执行结果:
enter
hello world
exit
enter和exit方法
1.with语句的表达式的作用是返回一个遵循特定协议的对象。具体来说,该对象必须定义一个__enter__方法和一个__exit__方法,且后者必须接受特定参数
2.除了传统的self参数,__enter__方法不接受任何其他参数。当对象返回时该方法立即执行,然后如果有as变量(as子句是可选项),返回值将被赋给as后面使用的变量。
3.一般来说,__enter__方法负责执行一些配置。另外一方面,__exit__方法带有3个位置参数(不包括传统的self参数):一个异常类型、一个异常实例和一个回溯。如果没有异常,这3个参数全被设置为None,但如果在代码块内有异常发生,则参数被填充。
#!/usr/bin/env python
#coding:utf-8
class ContextManager(object):
def __init__(self):
self.entered = False
def __enter__(self):
self.entered = True
return self
def __exit__(self, exc_type, exc_instance,traceback):
self.entered = False
cm = ContextManager()
print cm.entered
执行结果:
False
该上下文管理器只是返回自身和设置其entered变量,在进入时设置为True,退出时设置为False。
如果相同的ContextManager实例作为上下文管理器,观察它的entered属性会先变成True,然后在退出时再次变成False
#!/usr/bin/env python
#coding:utf-8
class ContextManager(object):
def __init__(self):
self.entered = False
def __enter__(self):
self.entered = True
return self
def __exit__(self, exc_type, exc_instance,traceback):
self.entered = False
cm = ContextManager()
with cm:
print cm.entered
print cm.entered
执行结果:
True
False
如果在其他地方不需要ContextManager实例,可以用with语句将其实例化。该方法可行的原因在于它的__enter__方法只返回了它本身。
#!/usr/bin/env python
#coding:utf-8
class ContextManager(object):
def __init__(self):
self.entered = False
def __enter__(self):
self.entered = True
return self
def __exit__(self, exc_type, exc_instance,traceback):
self.entered = False
# cm = ContextManager()
with ContextManager() as cm:
print cm.entered
# print cm.entered
执行结果:
True
异常处理
1.上下文管理器必须定义__exit__方法,该方法可以选择性地处理包装代码快中出现的异常,或处理其他需要关闭上下文管理状态的事情。
2.__exit__方法必须定义3个位置参数:异常类型、异常实例以及回溯选择。如果上下文管理器中的代码没有发生异常,则所有3个参数的值为None。
3.如果__exit__方法接收一个异常,就有处理这个异常的义务。从根本上讲,这个方法有3个可选项:
可以传播异常(因为会在__exit__完成后再次抛出异常)
可以终止异常
可以抛出不同的异常
4.可以通过让一个__exit__方法返回False实现异常的传播,或者通过让__exit__返回True终止异常。另外,如果__exit__抛出不同的异常,它将代替异常被发送出去。
何时编写上下文管理器
涉及确保某种资源以一种期望的方法被初始化或反初始化,或尽力去避免重复时可以使用上下文管理器
避免重复
当提到避免重复时,异常处理是最为常见的。上下文管理器能够传播和终止异常,这使得最适合将它与except子句放在同一个地方定义。
1.传播异常
__exit__方法只是向流程链上传播异常,这是通过返回False实现的,根本不需要与异常实例交互
例子1:
#!/usr/bin/env python
#coding:utf-8
class BubbleExceptions(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
print "Bubbling up exception: %s." % exc_val
return False
with BubbleExceptions():
print 5 + 5
执行结果:
10
在上下文管理器中运行普通代码块(不抛出异常)将不会做什么特殊的事情
例子2:
#!/usr/bin/env python
#coding:utf-8
class BubbleExceptions(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
print "Bubbling up exception: %s." % exc_val
return False
with BubbleExceptions():
print 5 / 0
执行结果:
Bubbling up exception: integer division or modulo by zero.
Traceback (most recent call last):
File "G:/PycharmProject/fullstack2/week1/test6.py", line 15, in <module>
print 5 / 0
ZeroDivisionError: integer division or modulo by zero
这里有2件需要注意的事情:
1.第一行(Bubbling up exception: integer division or modulo by zero.)由__exit__方法自身产生。它对应于__exit__方法中的print语句。
这意味着__exit__方法的确运行了且已完成。
2.因为该方法返回了False,所以被首先发送给__exit__的异常只是被重新抛出了。
2.终止异常
__exit__方法拥有的另一个选项就是终止它所接收的异常。下面的上下文管理终止所有可能发送给__exit__方法的异常(但是永远不要这样做):
例子1:
#!/usr/bin/env python
#coding:utf-8
class SuppressExceptions(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
print "SuppressException: %s." % exc_val
return True
with SuppressExceptions():
print 5+5
执行结果:
10
例子2:
#!/usr/bin/env python
#coding:utf-8
class SuppressExceptions(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
print "SuppressException: %s." % exc_val
return True
with SuppressExceptions():
print 5 / 0
执行结果:
SuppressException: integer division or modulo by zero.
注意:
1.首先且最明显的是回溯消失了。由于异常被__exit__方法处理了(终止),因此程序没有引发异常,继续执行。
2.第二个要注意的是没有任何返回值。当进入解释器时,尽管表达式 5 + 5返回了10,但引发异常的表达式 5/0根本不会显示值。异常在计算该值的过程中
引发,从而触发__exit__的运行。实际上永远不会返回任何值。另外,值得注意的是任何出现在5/0后面的代码都不会再执行。
3.处理特定异常类
一个简单的异常处理函数__exit__可以仅检查异常是否是特定异常类的实例,执行任何必要的异常处理,并根据是否获得其他类型的异常类返回True(或返回False)。
例子1:
#!/usr/bin/env python
#coding:utf-8
class HandleValueError(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if not exc_type:
return True
if issubclass(exc_type,ValueError):
print "Handling ValueError: %s" % exc_val
return True
return False
with HandleValueError(): #使用该上下文管理器并且在代码块内引发ValueError,则会看到想要的输出,之后异常终止
raise ValueError("Wrong value.")
执行结果:
Handling ValueError: Wrong value.
例子2:
#!/usr/bin/env python
#coding:utf-8
class HandleValueError(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if not exc_type:
return True
if issubclass(exc_type,ValueError):
print "Handling ValueError: %s" % exc_val
return True
return False
with HandleValueError(): #使用上下文管理器但引发不同类的异常,那么抛出该异常并输出回溯
raise TypeError("Wrong type.")
执行结果:
Traceback (most recent call last):
File "G:/PycharmProject/fullstack2/week1/test6.py", line 21, in <module>
raise TypeError("Wrong type.")
TypeError: Wrong type.
4.不包括的子类
如何完成类或实例的检查也可以更加灵活。假如想要捕获一个给定的异常类,但不希望显式地捕获它的子类。在传统的except代码块中不能这样做,也不该这样做。但是上下文管理器就能处理这样的极端情况。
例子1:
#!/usr/bin/env python
#coding:utf-8
class ValueErrorSubclass(ValueError):
pass
class HandleValueError(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if not exc_type:
return True
if exc_type == ValueError:#通过使用==来检查其类型,这意味着对ValueError的处理与之前一样,只是管理器不再处理ValueError的子类
print "Handling ValueError: %s " % exc_val
return True
return False
with HandleValueError():
raise ValueErrorSubclass("foo bar baz")
执行结果:
Traceback (most recent call last):
File "G:/PycharmProject/fullstack2/week1/test6.py", line 25, in <module>
raise ValueErrorSubclass("foo bar baz")
__main__.ValueErrorSubclass: foo bar baz
5.基于属性的异常处理
上下文管理器可以根据异常的类型来决定是否处理异常,与此类似,它还可以根据异常的属性来决定是否处理异常
更简单的语法:contextlib模块
contextlib模块的作用是提供更易用的上下文管理器,它是通过Generator实现的。contextlib中的contextmanager作为装饰器来提供一种针对函数级别的上下文管理机制,常用框架如下:
from contextlib import contextmanager @contextmanager def make_context(): print 'enter' try: yield "ok" except RuntimeError, err: print 'error', err finally: print 'exit' with make_context() as value: print value
输出为:
enter
ok
exit
其中,yield写入try-finally中是为了保证异常安全(能处理异常)as后的变量的值是由yield返回。yield前面的语句可看作代码块执行前操作,yield之后的操作可以看作在__exit__函数中的操作。
以线程锁为例:
from contextlib import contextmanager import threading lock = threading.Lock() @contextmanager def loudLock(): print('Locking') lock.acquire() yield print('Releasing') lock.release() with loudLock(): print('Lock is locked: %s' % lock.locked()) print('Doing something that needs locking')
执行结果:
Locking Lock is locked: True Doing something that needs locking Releasing
contextlib.nested:减少嵌套
对于:
with open(filename,mode) as reader:
with open(filename1,mode1) as writer:
writer.write(reader.read())
可以通过contextlib.nested进行简化:
with contextlib.nested(open(filename,mode),open(filename1,mode1)) as (reader,writer):
writer.write(reader.read())
在python 2.7及以后,被一种新的语法取代:
with open(filename,mode) as reader,open(filename1,mode1) as writer:
writer.write(reader.read())
contextlib.closing()
file类直接支持上下文管理器API,但有些表示打开句柄的对象并不支持,如urllib.urlopen()返回的对象。还有些遗留类,使用close()方法而不支持上下文管理器API。为了确保关闭句柄,需要使用closing()为它创建一个上下文管理器(调用类的close方法)。
#!/usr/bin/env python #coding:utf8 import contextlib class myclass(): def __init__(self): print '__init__' def close(self): print 'close()' with contextlib.closing(myclass()): print 'ok'
输出:
__init__
ok
close()