上下文管理器和else块

今天的主题是讨论以下特性:

* with语句和上下文管理器

* for、while、try语句的else子句

使用with语句会设置一个临时的上下文,交给上下文管理器对象控制,并负责清理上下文。这么做主要是为了避免样子代码,易于使用。除了自动关闭文件外,with还有很多其他用途。

else子句与with语句没有任何联系。

先做这个,在做那个:if语句之外的else块

这个语言特性不是什么秘密,但却没有得到应有的重视:else子句不仅能在if语句中使用,也能在for、while和try语句中使用。

else子句的行为如下:

for

  仅当for循环结束时(没有被break语句跳出),运行else块

while

  仅当while循环条件为False时(没有被break跳出),运行else块

try

  try块中没有异常抛出时,运行else块,且else子句抛出的异常不会被之前的except捕获。

笔者认为,else在这三个里面的意思与其真正作用有点矛盾,选择else作为关键字的确会令人有些苦恼,替换成then、final更好些。

1 for item in my_list:
2     if item == 'banana':
3         break
4 else:
5     print("over. no banana in my_list")
6 
7 """
8 如果my_list中包含'banana'那么for循环会被break跳出,从而不会打印"over. no banana in my_list“,如果my_list没有‘banana’,那么for循环正常结束,就会打印那句提示
9 """
1 try:
2     dangerous_call()
3 except OSError:
4     log('OSError...')
5 else:
6     after_call()

只有在try块不抛出异常的情况下,else块才会执行。

在python中,try/except不仅用于处理错误,还常用于控制流程。

EAFP

  取得原谅比获得许可容易(easier to ask for forgiveness than permission)。这是一种常见的 Python 编程风格,先假定存在有效的键或属性,如果假定不成立,那么捕获异常。这种风格简单明快,特点是代码中有很多 try 和 except 语句。与其他很多语言一样(如 C 语言),这种风格的对立面是 LBYL 风格

LBYL
  三思而后行(look before you leap)。这种编程风格在调用函数或查找属性或键之前显式测试前提条件。与 EAFP 风格相反,这种风格的特点是代码中有很多 if语句。在多线程环境中,LBYL 风格可能会在“检查”和“行事”的空当引入条件竞争。例如,对 if key in mapping: return mapping[key] 这段代码来说,如
果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代码就会失败。

上下文管理器和with块

with语句的目的是简化try/finally模式。这种模式用于执行某段代码执行完毕后的某项操作,如关闭文件、关闭管道、或者关闭socket连接等。通常用于释放资源或者还原状态。

上下文管理协议要实现__enter__方法,__exit__方法。with语句开始时会在上下文管理对象上调用__enter__方法,with语句运行结束后,会调用上下文管理对象的__exit__方法。

 1 class FileContext:
 2     def __init__(self, filename, mode='r'):
 3         self.filename = filename
 4         self.mode = mode
 5 
 6     def __enter__(self):
 7         print("__enter__")
 8         self.fp = open(self.filename, self.mode)
 9         return self.fp
10 
11     def __exit__(self, exc_type, exc_val, tb):
12         print("__exit__")
13         print("exc_type: %s, exc_val: %s, tb: %s" %(exc_type, exc_val, tb))
14         self.fp.close()
15         del self.fp
16         return True
17 
18 with FileContext("xxxx", 'w') as f:
19     print(f)
20     time.sleep(3)

在执行with后面的表达式时,会执行__enter__方法,然后用as句法让f引用__enter__方法返回的文件对象(这里要澄清一点with后面表达式得到是上下文管理对象,这个对象调用__enter__方法得到的返回值才是f)。不管控制流程以哪种方式退出with块,__exit__都会执行。

__exit__的三个参数如下:

exc_type

  异常类型,如ZeroDivisionError

exc_value

  异常实例,传递给异常构造方法的参数

traceback/tb

  traceback对象

__exit__方法如果返回值不是True,那么with语句中的任何异常都会向上冒泡。

contextlib模块中的实用工具

contextlib模块中有一些类和函数:

closing

  如果对象提供了close()方法,但没有实现__enter__/__exit__,这个函数会自动构建上下文管理器

suppress

  构建临时忽略指定异常的上下文管理器

@contextmanager

  这个装饰器把简单的生成器函数编程上下文管理器,这样就不用创建类去实现管理器协议了

ContextDecorator

  这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数

ExitStack

  这个上下文管理器能进入多个上下文管理器。with块结束时,ExitStack按照后进先出的原则一次调用栈中各个上下文管理器的__exit__方法。如果事先不知道with块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意目录下的所有文件。

这里面,使用最广泛的@contextmanager装饰器,因此要格外小心。这个装饰器很容易迷惑人,他跟迭代无关,但是使用了yield语句。

使用@contextmanager

@contextmanager装饰器能有效减少创建上下文管理器的样板代码量,我们无需编写一个类,还要实现__enter__和__exit__方法,只需实现一个带有yield语句的生成器,返回__enter__返回的值即可。

在使用@contextmanager装饰的生成器中,yield语句前的所有代码在with后的表达式中调用,yield返回的值作为as 后面的变量接收,yield以后的代码(想让__exit__完成的操作),在with块结束之后调用。

 1 @contextlib.contextmanager
 2 def openfile(filename, mode='r'):
 3     try:
 4         fp = None
 5         print("openfile(%s, %s)" %(filename, mode))
 6         fp = open(filename, mode)
 7         yield fp
 8     except FileNotFoundError:
 9         print("file not found")
10         yield fp
11     finally:
12         print("finally: ...")
13         if fp:
14             fp.close()
15 
16 with openfile('xxxxy') as f:
17     print(f)
18     time.sleep(2)

为了能够正常释放文件对象,先把它定义成None,然后在finally中,判断它的值。

本质上,@contextmanager装饰器会把函数包装成为实现__enter__和__exit__方法的上下文管理类(上下文管理对象必须遵守的上下文管理协议)。

这个类的__enter__方法的作用:

1)调用生成器函数,保存生成器对象(gen)

2)调用next(gen),执行到yield关键字处暂停

3)yield返回的值绑定到as后的变量上

with语句终止时,这个类的__exit__方法做了下面几件事:

1)检查有没有异常抛出,传给它的exc_type参数,如果有,调用gen.throw(exception),在生成器函数包含yield关键字哪行抛出异常

2)如果没有异常,调用next(gen),从生成器函数上次终止的yield语句后继续执行

为了告诉解释器异常已经得到处理了,__exit__方法应该返回True,此时解释器会压制异常。如果__exit__方法没有显式返回一个值,那么解释器得到是None,然后向上冒泡异常。然而,使用@contextmanager装饰器时,默认的行为时相反的,解释器会默认所有的异常已经得到处理了,因此应该压制异常。如果不想让@contextmanager压制异常,应该在被装饰的函数中重新显式的抛出异常。

@contextmanager装饰器方便、使用,把三个不同的python特性结合到了一起,装饰器、生成器、上下文管理。

posted on 2019-03-08 17:06  forwardFields  阅读(168)  评论(0编辑  收藏  举报

导航