当前位置: 代码迷 >> 综合 >> Fluent Python - Part15上下文管理器和 else 块
  详细解决方案

Fluent Python - Part15上下文管理器和 else 块

热度:81   发布时间:2024-03-07 01:42:20.0

本章要讨论的内容有:

  • with 语句和上下文管理器
  • for, while, try 语句的 else 子句

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

else 子句不仅能在 if 语句中使用,还能在for,while,try 语句中使用。

else 子句的行为如下:

  • for:仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)
  • while: 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止) 才运行 else 块。
  • try:仅当 try 块中没有异常抛出时才运行 else 块。官方文档还指出:“else 子句抛出的异常不会由前面的 except 子句处理”

在所有情况下,如果异常或者 return, break, 或 continue 语句导致控制权跳到了复合语句的主块之外,else 子块也会被跳过。

最后介绍Python的两个风格:

  • EAEP:取得原谅比获得许可容易(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] 这段代码来说,如果在测试之后,但在查找之前,另外一个线程从映射中删除了那个键,那么这段代码就会失败。这个问题可以使用锁或者 EAFP 风格解决。

下面探讨本章的主要话题:强大的 with 语句。

上下文管理器和 with 块

上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。

with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常,return 语句或 sys.exit() 调用而中止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。

上下文管理器协议包含 __enter____exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。

一些需要注意的点:

  • 执行 with 后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as 子句) 是在上下文管理器对象上调用 __enter__ 方法的结果。
  • 如果 __exit__ 方法返回 None, 或者 True 之外的值,with 块中的任何异常都会向上冒泡。

解释器调用 __enter__ 方法时,除了隐式的 self 之外,不会传入任何参数。传给 __exit__ 方法的三个参数列举如下。

  • exc_type: 异常类(例如 ZeroDivisionError).
  • exc_value: 异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取。
  • traceback: traceback对象。在 try/finally 语句中的 finally 块中调用 sys.exc_info(),得到的就是 __exit__接受的这三个参数。鉴于 with 语句是为了取代大多数 try/finally 语句,而且通常需要调用 sys.exc_info() 来判断做什么清理操作,这种行为是合理的。

contextlib 模块中的实用工具

contextlib 模块中海油一些类和其他函数,实用范围很广。

  • closing: 如果对象提供了 close() 方法,但没有实现 __enter__/__exit__协议,那么可以使用这个函数构建上下文管理器。
  • suppress: 构建临时忽略指定异常的上下文管理器。
  • @contextmanager: 这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类来实现管理器协议了。
  • ContextDecorator: 这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文运行整个函数。
  • ExitStack: 这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中的各个上下文管理器的 __exit__ 方法。如果事先不知道 with 块要进入多个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。

显然,在这些实用工具中,使用最广泛的是 @contextmanager 装饰器,因此要格外留心。这个装饰器也有迷惑人的一面,因为它与迭代无关,却要使用 yield 语句。由此可以引出协程。

使用 @contextmanager

@contextmanager 装饰器能减少创建上下管理器的样板代码量,因为不用编写一个完整的类,定义 __enter____exit__ 方法,而值需事先有一个 yield 语句的生成器,生成想让 __enter__ 方法返回的值。

在使用 @contextmanager 装饰的生成器中, yield 语句的作用是把函数的定义体分成两部分: yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 __exit__方法时)执行。

import contextlib@contextlib.contextmanager
def looking_glass():import sysoriginal_write = sys.stdout.writedef reverse_write(text):original_write(text[::-1])sys.stdout.write = reverse_writeyield 'JABBERWOCKY'sys.stdout.write = original_writewith looking_glass() as what:print("Alice, Kitty and Snowdrop")print(what)""" output: pordwonS dna yttiK ,ecilA YKCOWREBBAJ """

with 块终止时,__exit__ 方法会做一下几件事。

  1. 检查有没有把异常传给 exc_type;如果有,调用 gen.throw(exception), 在生成器函数定义体中包含 yield 关键字那一行抛出异常。
  2. 否则,调用 next(gen), 继续执行生成器函数定义体重 yield 语句之后的代码。

上述代码有一个严重的错误:如果在 with 块中抛出了异常,Python解释器会将其捕获,然后在 looking_glass 函数的 yield 表达式里再次抛出。但是,那里没有处理错误的代码,因此 looking_glass 函数会中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处于无效状态。

正确写法如下:

import contextlib@contextlib.contextmanager
def looking_glass():import sysoriginal_write = sys.stdout.writedef reverse_write(text):original_write(text[::-1])sys.stdout.write = reverse_writetry:yield 'JABBERWOCKY'except ZeroDivisionError:msg = 'Please Do Not divide by zero!'finally:sys.stdout.write = original_writewith looking_glass() as what:print("Alice, Kitty and Snowdrop")print(what)

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

使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。

下一章我们会讨论协程!