WeeklyPEP-2-PEP 343-with 语句-overview

请注意:本文编写于 ,其中某些信息可能已经失去时效性。

前言

本文的主体内容大部分来自对 PEP 343 原文的翻译,其余部分为本人对原文的理解,在整理过程中我没有刻意地区分翻译的部分和我个人理解的部分,这两部分内容被糅杂在一起形成了本文。因此,请不要带着「本文的内容是百分之百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 PEP 343 的原文加以确认。

摘要

本 PEP 为 Python 增加了一个新的语法:with,它能够更加简便的代替标准的 try/finally 语法。

在本 PEP 中,上下文管理器 提供了 __enter__() 和 __exit__() 方法,分别在进入和退出 with 语句时被调用。

历史背景

本 PEP 最初是由 Guido 以第一人称撰写,随后由 Nick Coghlan 更新那些后来在 python-dev 上的讨论。

在对 PEP 340 及其代替方案进行大量讨论后,Guido 决定撤销 PEP 340 并在 PEP 310 的基础上提出一个新的版本。此版本增加了一个 throw() 方法用于在 暂停的生成器 中引发异常以及一个 close() 方法用来抛出 GeneratorExit 异常,并将语法关键字修改为 with(在 PEP 340 中定义的是 block)。

PEP 343 被正式通过后,以下 PEP 由于内容重叠被驳回或撤销:

  1. PEP 310:最初的 with 语法的提案
  2. PEP 319:它所描述的场景可以通过提供合适的 with 语法实现
  3. PEP 340
  4. PEP 346

Python Wiki 上对本 PEP 的早期版本进行过一些讨论。

提案动机

PEP 340 中阐述了大量优秀的想法,例如:使用生成器作为 block 语法的模板,添加异常处理和 finalization(个人感觉可以理解为析构方法) 给生成器等等。除了赞同,它也因为底层是循环结构的事实遭到了很多人的反对。使用循环结构意味着 block 语法体内的 break 和 continue 会扰乱正常的代码逻辑带来不确定性,即使它被用来作为一个非循环资源的管理工具。

Raymond Chen一篇对流程控制宏提出反对的文章 让 Guido 做出了最终决定——放弃 PEP 340。Raymond 的文章中有一个观点:将流程控制隐藏在宏中会让代码变得更难让人读懂也更难维护,Guido 发现这个观点在 Python 和 C 都适用,并由此意识到 PEP 340 提出的 templates 会隐藏几乎所有控制流程。例如,PEP 340 的 examples 4 中的 auto_retry 函数只能捕获异常并且重复 block 最多三次(而这一点被隐藏在了语法的内部)。

对比来看 PEP 310 的 with 语法并没有隐藏流程控制,虽然 finally 会暂时地中止控制流,但在最后(finally 内的内容执行完毕后)控制流会继续执行就好像 finally 不存在一样。

PEP 310 中粗略地提出了这样的语法(其中 VAR 是可选的):

1
2
with VAR = EXPR:
BLOCK

上面的语法大致上可以翻译成这样:

1
2
3
4
5
6
7
VAR = EXPR
VAR.__enter__()

try:
BLOCK
finally:
VAR.__exit__()

但当在 PEP 310 的语法基础上实现如下代码时:

1
2
3
with f = open("/ect/passwd"):
BLOCK1
BLOCK2

按照前面给的示例这段代码大致可以被翻译成这样:

1
2
3
4
5
6
7
8
9
f = open("/etc/passwd")
f.__enter__()

try:
BLOCK1
finally:
f.__exit__()

BLOCK2

上面的代码好像默认了 BLOCK1 的部分会没有异常地顺利的执行,然后 BLOCK2 部分会被紧接着调用。但如果在 BOLCK1 中有异常抛出或执行了一个 non-local goto(例如:break, continue, return),BLOCK2 就无法被顺利执行。with 语法在尾部增加的魔法(__exit__())并不能解决这一问题。

你也许会问:如果在 __exit__() 方法中抛出了一个异常会怎样?
如果真的发生了,那一切就都完了,但这并不会比其他情况下引发的异常更糟糕。

异常的本质就是它可以在代码的任意位置被抛出,而你只能忍受这一点。即使你写的代码不会抛出任何异常,一个 KeyboardInterrupt 异常仍然会导致它在任意两个虚拟机器操作码之间退出(个人理解:哪怕你的程序没有任何问题,正在正常执行的程序也可能因为你的强制退出行为而退出,例如常见的 Ctrl + c)。

上面的这些讨论以及 PEP 310 表现出来的特性让 Guido 开始更倾向于使用 PEP 310 的语法,但是他还是希望能够实现 PEP 340 中提出的:使用生成器作为获取和释放锁或打开和关闭文件等抽象概念的「模板」。这是一个很有用的功能,通过 PEP 340 的示例 就能够有所了解。

受 Phillip Eby 对 PEP 340 一个反对建议的启发,Guido 尝试创建一个装饰器,将一个合适的生成器变成一个具有必要的 __enter__() 和 __exit__() 方法的对象。但在实现过程中他遇到一个障碍:处理 locking example 并不困难,但是处理 opening example 几乎是不可能的。他是这样定义语法模板的:

1
2
3
4
5
6
7
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()

并且可以被这样使用:

1
2
with f = opening(filename):
...read data from f...

问题是在 PEP 310 中,EXPR 表达式的结果会被直接分配给 VAR,然后在退出 BLOCK1 时调用 VAR 的 __exit__() 方法。但在这里,VAR 显然需要得到被打开的文件对象,这就意味着 __exit__() 必须是文件对象上的一个方法。

虽然这个问题可以通过代理类来解决,但这种解决方案并不优雅。在思考之后,Guido 意识到:只需要稍微改动语法模板就可以轻松地编写出所需的装饰器。这个改动就是:将 VAR 设置为 __enter__() 方法的调用结果,并且保存 EXPR 的值以便后面调用它的 __exit__() 方法。此时装饰器就能返回一个 wrapper 类的实例,实例的 __enter__() 方法调用生成器的 next() 方法返回 next() 所返回的内容。wrapper 类实例的 __exit__() 方法会再次调用生成器的 next() 方法并(期望)抛出一个 Stoplteration 异常。详情可以看下面的 生成器装饰器一节

所以现在最后的障碍是 PEP 310 的语法:

1
2
with VAR = EXPR:
BLOCK1

不应该使用赋值操作而应该使用隐式操作,因为本质上并不是将 EXPR 赋值给 VAR。参考 PEP 340 的实现,可以修改为:

1
2
with EXPR as VAR:
BLOCK1

在提案的讨论中还能够看出,开发者们普遍希望能够在生成器中捕获到异常,即使仅是拿来做日志。但生成器是不允许 yield 其他值的,因为 with 语法不应该被设计成一个循环(抛出一个不同的异常是稍微能够被接受的)。

为了实现这个需求,一个新的生成器方法 throw() 被提出,它将接收一到三个代表异常的可选参数(type, value, traceback)并且在 生成器暂停时 抛出它。

在提出 throw() 方法后另一个生成器方法 close() 也自然而然地被提出了。close() 方法能够通过一个名为 GeneratorExit 的特殊异常调用 throw() 方法。这个行为相当于告诉生成器准备退出,并且在此基础上还有一个小小的改进,即在生成器被垃圾回收机制回收时会自动触发 close() 方法。

自此以后,我们就可以在 try-finally 语法中插入 yield 语法了,因为我们现在能够保证 finally 语句最后一定会被执行。但还有一些关于 finalization apply 的常见警告:进程可能在没有析构任何对象的情况下突然终止,而且对象可能会因为应用中的循环或内存泄漏而永远存在(与被 GC 控制 的 Python 的循环或内存泄漏相反)。

注意,我们并不能保证 finally 部分会在生成器对象无引用后被立即执行,尽管 CPython 中是这样实现的。这与自动关闭文件类似:虽然在 CPython 中对象的最后一个引用消失后会立即删除该对象,但其他的 GC 算法未必做了同样的实现。

可以去 PEP 342 中找那些生成器被修改的细节。

语法规范

一个新的声明被提出,它的语法是:

1
2
with EXPR as VAR:
BLOCK

其中 with 和 as 是新的关键字。EXPR 是一个任意的表达式(但不能是一个表达式列表),VAR 是一个单一的分配目标,它不能是逗号分割的变量序列,但可以被括号包裹的逗号分割的变量序列(这个限制使得未来的语法有可能拓展为多个逗号分割的资源,每个资源都有对应的 as 语句)。

as VAR 部分是可选的,上述声明被翻译为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True

try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
# 如果 BLCOK 部分出现异常
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
# 如果 exit() 返回 true,则异常会被忽略
finally:
# The normal and non-local goto case are handled here
# 如果 BLOCK 没有异常或者执行了 non-local goto
if exc:
exit(mgr, None, None, None)

其中小写的变量(mgr, exit, value, exc)都是内部变量,用户不能访问,它们大概率会被实现为特殊的寄存器或 stack positions。sys.exc_info 的详细信息可以参考 sys.exc_info 章节

上述代码翻译细节是为了规定准确的语义。如果相关方法没有像预期那样被找到,解释器将会抛出 AttributeError 异常。同样,如果任何一个调用抛出了一个异常,其效果与上述代码一致。最后,如果 BLOCK 包含 non-local goto 语句,那么 __exit__() 方法会像 BLOCK 被正常执行完一样被调用,并带有三个 None 参数。也就是说,这些「伪异常」不会被 __exit__() 视为异常。

如果 as VAR 部分的语法被省略,翻译中 VAR = 的部分也会被省略,但 mgr.__enter() 仍被调用。

mgr.__exit__() 的调用惯例如下:

  • 如果 BLOCK 中的内容正常执行完毕或某个 non-local goto 被触发,执行到 finally 语法块时 mgr.__exit__() 将被调用,并带有三个 None 参数。
  • 如果 BLOCK 中的内容引发了异常,执行到 finally 语法块时,mgr.__exit__() 将被调用,并被传递三个分别代表异常 type, value 和 traceback 的参数。

重要的是:如果 mgr.__exit__() 返回的是 True,异常就会被忽略。也就是说,如果 mgr.__exit__() 返回 True,那么在 with 语句之后的下一个语句仍会被执行,即使 with 语法内部发生了异常。

然而,如果 with 语法被某个 non-local goto 中断,当 mgr.__exit__() 返回时,这个 non-local return 将在不考虑返回值的情况下被恢复。这样做的目的是使 mgr.__exit__() 能够在不被频繁误触发的情况下实现忽略异常抛出(因为 mgr.__exit__() 的默认返回值是 None 相当于 Flase,能够使异常被重新抛出)。

实现忽略异常抛出功能的主要目的是使编写 @contextmanger 装饰器成为可能。在能够忽略异常抛出的情况下实现的 @contextmanger 装饰器能够使一个被装饰的生成器内部的 try/except 块的行为就像生成器的主体在 with 语法的位置上被在线拓展一样。

将异常细节传递给 __exit__() 的动机,与 PEP 310 中无参数的 __exit__() 一致,具体可以参考示例章节的 transaction:数据库事务管理。在本示例中,生成器函数必须根据是否发生异常来决定是否进行事物回滚。在这种情况下,传递完整异常信息要比使用一个布尔值来标记是否发生异常更具有可拓展性,例如为异常记录工具提供接口。

依靠 sys.exc_info() 来获取异常信息的做法被否定了,因为 sys.exc_info() 的语义非常复杂,它完全有可能返回一个很久以前就被捕获的异常信息。还有人提议增加一个布尔值来区分顺利执行完 BLOCK 块和 BLOCK 块被 non-local goto 中断。这个提议也被否定了,因为它太过复杂且没有必要。对于数据库事务回滚的决定来说,non-local goto 应该被视为异常。

为了使直接操作上下文管理器的 Python 代码的上线文变得简单,__exit__() 方法不应该重新抛出传递给它们的异常。应该总是由 __exit__() 方法的调用者负责决定何时重新引发异常。

因此,调用者可以通过是否抛出异常来区分 __exit__() 是否执行失败:

  • 如果 __exit__() 未抛出异常,代表方法本身执行成功,无论被传入的异常是否被忽略;
  • 如果 __exit__() 抛出异常,代表方法本身执行失败。

因此,在实现 __exit__() 方法时应该避免抛出异常,除非真的存在异常(不需要避免抛出那些被传入的异常)。

生成器装饰器

PEP 342 通过后,我们就可以通过编写一个装饰器,让被此装饰器装饰的生成器可能被 with 语法使用,且此生成器刚好 yeild 一次。下面是完成此类装饰器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class GeneratorContextManager(object):

def __init__(self, gen):
self.gen = gen

def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")

def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
# only re-raise if it's not the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But
# throw() has to raise the exception to signal
# propagation, so thi fixes the impedance mismatch
# between the throw() protocol and the __exit__()
# protocol .
# 只有当它不是传递给 throw() 的异常时才会重新被抛出,
# 因为 __exit__() 只能在方法本身执行失败的时候抛出异常
# 但是 throw() 必须引发异常来传递信号
# 所以 thi 修复了 throw() 协议和 __exit__() 协议之间的不协调

if sys.exc_info()[1] is not value:
raise

def contextmanager(func):
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper

这个装饰器可以被这样使用:

1
2
3
4
5
6
7
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here(howere unlikely)

对此装饰器更加完善的实现已经成为 标准库的一部分

标准库中存在的上下文管理器

可以给文件、套接字和锁等等对象添加 __enter__() 和 __exit__() 方法,这样我们就可以像下面这样操作这些对象:

1
2
with locking(myLock):
BLOCK

也可以简写成

1
2
with myLock:
BLOCK

但是使用过程中应该谨慎,它可能会导致类似的错误:

1
2
3
4
5
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2

上面的代码并不会像人们想象的那样顺利的执行,因为 f 在进入 BLCOK2 之前被就已经被关闭了。

还有很多类似上面示例中的错误,例如:

  • 使用上面提到的 @contextmanager 装饰器装饰的生成器,会在第二次被 with 语法调用 f.__enter__() 时抛出 RuntimeError 异常;
  • 如果 __enter__ 在一个 closed 文件对象上被调用,也会引发类似的错误。

对于 Python 2.5,以下类型已经被确定为上下文管理器:

1
2
3
4
5
6
7
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore

一个上下文管理器也将被添加到 decimal 模块中,以支持在 with 语句中使用本地 decimal arithmetic 上下文,当 with 语句退出时自动回复原始上下文。

标准术语

本 PEP 提议将由 __enter__() 和 __exit__() 方法组成的协议称为「上下文管理协议」,而实现该协议的对象被称为「上下文管理器」。

语句中紧跟 with 关键字的表达式是一个「上下文表达式」。

with 语句主体中的代码和 as 关键字后面的变量名(或名称)没有特殊的术语。可以使用一般的术语「statement body」和「target list」,如果这些术语不清楚的话,可以用 with 或 with statement 作为前缀。

鉴于 decimal 模块的算术上下文等对象的存在,使得单独使用「上下文」这个术语可能存在歧义。如果有必要,可以用「上下文管理器」来表示由上下文表达式创建的具体对象,用「运行时上下文」或(最好是)「运行时环境」来表示由上下文管理器进行的实际状态修改,从而使描述更加具体。

当简单地讨论 with 语句的使用时,由于上下文表达式已经全面描述了对运行时环境所做的修改,因此此时这种模糊性不应该太重要。但在讨论 with 语句本身的机制以及如何实际实现上下文管理器时,这些名词之间的区别就尤为重要。

如何缓存上下文管理器

许多上下文管理器(如文件和基于生成器的上下文)都是一次性使用的对象。一旦 __exit__() 方法被调用,上下文管理器将不可复用 (例如,文件已被关闭,或者底层生成器已执行完毕)。

为每个 with 语句创建一个新的上下文管理器对象是避免多线程代码或嵌套的 with 语句试图使用同一个上下文管理器问题的最简单的解决方案。标准库中所有可复用的上下文管理器都来自 threading 模块,它们都针对线程和嵌套使用所产生的问题进行过相应设计。

这意味着,为了在多个 with 语句中重复使用一个带有特定初始化参数的上下文管理器,通常需要将其存储在一个零参数的可调用对象中,然后在每个语句的上下文表达式中调用,而不是直接缓存上下文管理器。

当这种限制不适用时,受影响的上下文管理器的文档应该明确说明这一点。

被拒绝的选项

几个月来,PEP 都为了避免隐藏控制流程而禁止忽略异常抛出,但在实施过程中发现这是一个很难避免的麻烦,因此 Guido 重启了这个功能。

本 PEP 的另一个核心是提出了一个 __context__() 方法,类似于 iterable’s __iter__() 方法。这引起了无休止的问题和术语讨论。不断解释 __content__() 方法相关问题的过程中产生的新问题让 Guido 最终彻底删除了这个概念。

直接使用 PEP 342 的生成器 API 来定义 with 语法 也被简短地讨论过,但很快就被否定了,因为如果这么做会使得编写非生成器的上下文管理器程序变得异常困难。

示例

基于生成器的例子依赖于 PEP 342。另外,有些例子在实践中是不必要的,因为现有的对象,如 threading.RLock,能够直接用于 with 语句中。

示例中上下文的名称所使用的时态不是任意的:

  • 过去时态(ed)表示一个在 __enter__ 方法中完成,在 __eixt__ 方法中撤销的动作;
  • 进行时态(ing)表示一个在 __exit__ 方法中完成的动作。

locked:锁管理

在语句开始时获得锁,离开时释放锁:

1
2
3
4
5
6
7
@contextmanager
def locked(lock):
lock.acquire()
try:
yield
finally:
lock.release()

可以这样使用:

1
2
3
4
5
6
with locked(myLock):
# Code here executes with myLock held. The lock is
# guaranteed to be released when the block is left (even
# if via return or by an uncaught exception).
# 这里的代码是在持有 myLock 的情况下被执行的。
# 这个锁被确保在离开 with 语句之后被释放,即使在执行过程中有中断或异常

opened:文件管理

在语句开始时通过特定模式打开文件,离开时关闭文件:

1
2
3
4
5
6
7
@contextmanager
def opened(filename, mode="r"):
f = open(filename, mode)
try:
yield f
finally:
f.close()

可以这样使用:

1
2
3
with opened("/etc/passwd") as f:
for line in f:
print line.rstrip()

transaction:数据库事务管理

提交或回滚数据库事务:

1
2
3
4
5
6
7
8
9
10
@contextmanager
def transaction(db):
db.begin()
try:
yield None
except:
db.rollback()
raise
else:
db.commit()

class locked:不使用生成器实现的锁管理

在不使用生成器情况下重写 例 1

1
2
3
4
5
6
7
8
9
class locked:
def __init__(self, lock):
self.lock = lock

def __enter__(self):
self.lock.acquire()

def __exit__(self, type, value, tb):
self.lock.release()

这个例子很容易修改成其他相对无状态的例子,这表明,如果不需要保留特殊状态,很容易避免对生成器的依赖。

stdout_redirected:输出重定向管理

暂时重定向 stdout:

1
2
3
4
5
6
7
8
@contextmanager
def stdout_redirected(new_stdout):
save_stdout = sys.stdout
sys.stdout = new_stdout
try:
yield None
finally:
sys.stdout = save_stdout

可以这样使用:

1
2
3
with opened(filename, "w") as f:
with stdout_redirected(f):
print "Hello world"

这样实现不能保证线程安全,但如果手动实现相同的动作也无法保证线程安全。在单线程程序中(例如,在脚本中),这是一种流行的处理方案。

opened_w_error:文件(__enter__ 同时返回两个值)

opened() 的一个变体,能够同时返回文件句柄和异常内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
@contextmanager
def opened_w_error(filename, mode="r"):
try:
f = open(filename, mode)
except IOError, err:
# 使用元组同时产生两个返回值
yield None, err
else:
try:
# 使用元组同时产生两个返回值
yield f, None
finally:
f.close()

可以这样使用:

1
2
3
4
5
6
# 在 as 后使用元组接收返回值
with opened_w_error("/etc/passwd", "a") as (f, err):
if err:
print "IOError:", err
else:
f.write("guido::0:0::/:/bin/sh\n")

signal:信号拦截

另一个有用的例子是阻断信号的操作,实现代码如下:

1
2
3
4
5
import signal

with signal.blocked():
# code executed without worrying about signals
# 执行的代码不用担心信号问题

可以传入一个要屏蔽的信号列表,默认情况下,会屏蔽所有的信号。

decimal

decimal 上下文,这里有一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
import decimal

@contextmanager
def extra_precision(places=2):
c = decimal.getcontext()
saved_prec = c.prec
c.prec += places
try:
yield None
finally:
c.prec = saved_prec

使用示例(改编自 Python Library Reference):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def sin(x):
# Return the sine of x as measured in radians.
# 返回以弧度为单位的x的正弦。
with extra_precision():
i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
while s != lasts:
lasts = s
i += 2
fact *= i * (i-1)
num *= x * x
sign *= -1
s += num / fact * sign
# The "+s" rounds back to the original precision,
# so this must be outside the with-statement:
return +s

another decimal

一个简单的 decimal 模块的上下文管理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@contextmanager
def localcontext(ctx=None):
"""Set a new local decimal context for the block"""
# Default to using the current context
if ctx is None:
ctx = getcontext()
# We set the thread context to a copy of this context
# to ensure that changes within the block are kept
# local to the block.
newctx = ctx.copy()
oldctx = decimal.getcontext()
decimal.setcontext(newctx)
try:
yield newctx
finally:
# Always restore the original context
decimal.setcontext(oldctx)

使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from decimal import localcontext, ExtendedContext

def sin(x):
with localcontext() as ctx:
ctx.prec += 2
# Rest of sin calculation algorithm
# uses a precision 2 greater than normal
return +s # Convert result to normal precision

def sin(x):
with localcontext(ExtendedContext):
# Rest of sin calculation algorithm
# uses the Extended Context from the
# General Decimal Arithmetic Specification
return +s # Convert result to normal context

closing:object-closing 上下文管理器

通用「object-closing」上下文管理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class closing(object):
def __init__(self, obj):
self.obj = obj

def __enter__(self):
return self.obj

def __exit__(self, *exc_info):
try:
close_it = self.obj.close
except AttributeError:
pass
else:
close_it()

可以用来确切地关闭任何有 close 方法的对象,无论是文件、生成器还是其他东西。它也可以在不确定对象是否需要关闭时使用(例如,一个接受任意 iterable 的函数):

1
2
3
4
5
6
7
8
9
# emulate opening():
with closing(open("argument.txt")) as contradiction:
for line in contradiction:
print line

# deterministically finalize an iterator:
with closing(iter(data_source)) as data:
for datum in data:
process(datum)

Python 2.5 的 contextlib 模块包含 这个上下文管理器的实现(3.x 版本也保留了)

released:锁管理

PEP 319 给出了一个用例,即使用 release 上下文来暂时释放先前获得的锁。

可以通过交换 acquisition() 和 release() 的调用顺序,实现上面的 locked:锁管理

实现代码如下:

1
2
3
4
5
6
7
8
9
class released:
def __init__(self, lock):
self.lock = lock

def __enter__(self):
self.lock.release()

def __exit__(self, type, value, tb):
self.lock.acquire()

使用案例:

1
2
3
4
5
6
with my_lock:
# Operations with the lock held
with released(my_lock):
# Operations without the lock
# e.g. blocking I/O
# Lock is held again here

nested:嵌套使用

一个「嵌套」的上下文管理器,它能自动地将提供的上下文从左到右嵌套,以避免过度缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@contextmanager
def nested(*contexts):
exits = []
vars = []
try:
try:
for context in contexts:
exit = context.__exit__
enter = context.__enter__
vars.append(enter())
exits.append(exit)
yield vars
except:
exc = sys.exc_info()
else:
exc = (None, None, None)
finally:
while exits:
exit = exits.pop()
try:
exit(*exc)
except:
exc = sys.exc_info()
else:
exc = (None, None, None)
if exc != (None, None, None):
# sys.exc_info() may have been
# changed by one of the exit methods
# so provide explicit exception info
raise exc[0], exc[1], exc[2]

使用案例

1
2
with nested(a, b, c) as (x, y, z):
# Perform operation

等同于:

1
2
3
4
with a as x:
with b as y:
with c as z:
# Perform operation

Python 2.5的 contextlib 模块包含这个上下文管理器的实现(没有在 3.x 的文档中找到同名方法)。

实现过程

本 PEP 最初是由 Guido 在 2005.06.27 的 EuroPython 主题演讲中接受的。后来它被再次接受,并加入了 __context__ 方法。

本 PEP 是在 Python 2.5a1 的 Subversion 中实现的,在 Python 2.5b1 中删除了 __context__()方法。

附录

生成器状态

可以通过 inspect.getgeneratorstate(generator) 查看生成器状态:

GEN_CREATED: Waiting to start execution.
GEN_RUNNING: Currently being executed by the interpreter.
GEN_SUSPENDED: Currently suspended at a yield expression.
GEN_CLOSED: Execution has completed.

sys.exc_info

本函数返回的元组包含三个值,它们给出当前正在处理的异常的信息。返回的信息仅限于当前线程和当前堆栈帧。如果当前堆栈帧没有正在处理的异常,则信息将从下级被调用的堆栈帧或上级调用者等位置获取,依此类推,直到找到正在处理异常的堆栈帧为止。此处的「处理异常」指的是「执行 except 子句」。任何堆栈帧都只能访问当前正在处理的异常的信息。

如果整个堆栈都没有正在处理的异常,则返回包含三个 None 值的元组。否则返回值为 (type, value, traceback)。它们的含义是:

  • type:正在处理的异常类型(它是 BaseException 的子类);
  • value:异常实例(异常类型的实例);
  • traceback:一个 回溯对象,该对象封装了最初发生异常时的调用堆栈。

参考

  1. PEP 343 原文
  2. PEP 310 原文
  3. PEP 340 原文
  4. Raymond: A rant against flow control macros
  5. Brett Cannon: Unravelling the `with` statement
  6. Python 官方文档-Python标准库-sys 库
  7. Python 官方文档-数据模型-回溯对象