python 中 with 关键字

with 关键字

在 python 关于 with 关键字,中我们一般会这样写。

1
2
with open('file.txt', 'w') as f:
f.write('hello world!')

在这段代码中,无论with中的代码块在执行的过程中发生任何情况,文件最终都会被关闭。如果代码块在执行的过程中发生了一个异常,那么在这个异常被抛出前,程序会先将被打开的文件关闭。

with 关键字一般执行过程

一段基本的 with 表达式其结构是这样的:

1
2
with EXPR as VAR:
BLOCK

其中:EXPR可以是任意表达式;as VAR是可选的。其一般的执行过程是这样的:

  • 计算EXPR,并获取一个上下文管理器。
  • 上下文管理器的__exit()__方法被保存起来用于之后的调用。
  • 调用上下文管理器的__enter()__方法。
  • 如果with表达式包含as VAR,那么EXPR的返回值被赋值给VAR。
  • 执行BLOCK中的表达式。
  • 调用上下文管理器的__exit()__方法。如果BLOCK的执行过程中发生了一个异常导

致程序退出,那么异常的type、value和traceback(即sys.exc_info()的返回值)将作为参数传递给__exit()__方法。否则,将传递三个None。

将这个过程用代码表示,是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mgr = (EXPR)
exit = type(mgr).__exit__ # 这里没有执行
value = type(mgr).__enter__(mgr)
exc = True

try:
try:
VAR = value # 如果有 as VAR
BLOCK
except:
exc = False
if not exit(mgr, *sys.exc_info()):
raise
finally:
if exc:
exit(mgr, None, None, None)

这个过程有几个细节:

  • 如果上下文管理器中没有__enter()__或者__exit()__中的任意一个方法,那么解释器会抛出一个AttributeError。
  • 在BLOCK中发生异常后,如果__exit()__方法返回一个可被看成是True的值,那么这个异常就不会被抛出,后面的代码会继续执行。

实现上下文管理器类

实现一个类

第一种方法是实现一个类,其含有一个实例属性db和上下文管理器所需要的方法__enter()__和__exit()__。

1
2
3
4
5
6
7
8
9
10
11
12
class transaction(object):
def __init__(self, db):
self.db = db

def __enter__(self):
self.db.begin()

def __exit__(self, type, value, traceback):
if type is None:
db.commit()
else:
db.rollback()

使用生成器装饰器

在 Python 的标准库中,有一个装饰器可以通过生成器获取上下文管理器。使用生成器装饰器的实现过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from contextlib import contextmanager

@contextmanager
def transaction(db):
db.begin()

try:
yield db
except:
db.rollback()
raise
else:
db.commit()

contextmanager 实现

详解

  • Python 解释器识别到yield关键字后,def会创建一个生成器函数替代常规的函数(在类定义之外我喜欢用函数代替方法)。
  • 装饰器contextmanager被调用并返回一个帮助函数,这个帮助函数在被调用后会生成一个GeneratorContextManager实例。最终with表达式中的EXPR调用的是由contentmanager装饰器返回的帮助函数。
  • with表达式调用transaction(db),实际上是调用帮助函数。帮助函数调用生成器函数,生成器函数创建一个生成器。
  • 帮助函数将这个生成器传递给GeneratorContextManager,并创建一个GeneratorContextManager的实例对象作为上下文管理器。
  • with表达式调用实例对象的上下文管理器的__enter()__方法。
  • __enter()__方法中会调用这个生成器的next()方法。这时候,生成器方法会执行到yield db处停止,并将db作为next()的返回值。如果有as VAR,那么它将会被赋值给VAR。
  • with中的BLOCK被执行。
  • BLOCK执行结束后,调用上下文管理器的__exit()__方法。__exit()__方法会再次调用生成器的next()方法。如果发生StopIteration异常,则pass。
  • 如果没有发生异常生成器方法将会执行db.commit(),否则会执行db.rollback()。

代码实现

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
def contextmanager(func):
def helper(*args, **kwargs):
return GeneratorContextManager(func(*args, **kwargs))
return helper

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:
pass
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:
if sys.exc_info()[1] is not value:
raise

一些例子

锁机制

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

标准输出重定向

1
2
3
4
5
6
7
8
9
10
11
12
@contextmanager
def stdout_redirect(new_stdout):
old_stdout = sys.stdout
sys.stdout = new_stdout
try:
yield
finally:
sys.stdout = old_stdout

with open("file.txt", "w") as f:
with stdout_redirect(f):
print "hello world"