方式一:
为了让一个对象兼容 with
语句,你需要实现 __enter__ ()
和 __exit__ ()
方法。例如,考虑如下的一个类,它能为我们创建一个网络连接:
# -*- coding: utf-8 -*-
"""
上下文管理器协议 with 语句
————enter————
————exit————
"""
from functools import partial
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.sock = None
def __enter__(self): # with open(...) as fp: 调用这个方法,返回值就是fp
if self.sock is not None:
raise RuntimeError('Already connected')
self.sock = socket(self.family, self.type)
self.sock.connect(self.address)
return self.sock
def __exit__(self, exc_ty, exc_val, tb): # 不管正常还是异常,都调用这个方法,关闭套接字
self.sock.close()
self.sock = None
conn = LazyConnection(('www.python.org', 80))
with conn as s:
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.python.org\r\n')
s.send(b'\r\n')
resp = b''.join(iter(partial(s.recv, 8192), b''))
print(resp)
编写
上下文管理器的主要原理是你的代码会放到 with
语句块中执行。 当出现 with
语句的时候,对象的 __enter__ ()
方法被触发
,它返回的值
(如果有的话)
会被赋值
给as 声明
的变量
。然后, with
语句块里面的代码开始执行
。最后, __exit__ ()
方法被触发
进行清理工作
(释放资源
)。
不管 with
代码块中发生什么
,上面的控制流
都会执行完
,就算代码块
中发生了异常
也是一样的。事实上, __exit__ ()
方法的三个参数
包含了异常类型
、 异常值
和追溯信息
(如果有的话)。 __exit__ ()
方法能自己决定
怎样利用这个异常信息
,或者忽略
它并返回一个 None
值。如果 __exit__ ()
返回 True
,那么异常
会被清空
,就好像什么都没发生
一样, with
语句后面的程序
继续在正常执行。
还有一个细节问题就是 LazyConnection
类是否
允许多个 with 语句
来嵌套
使用连接
。很显然,上面的定义中一次只能允许一个
socket
连接,如果正在使用
一个 socket
的时候又重复使用 with
语句,就会产生一个异常
了。不过你可以像下面
这样修改下上面的实现来解决
这个问题:
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.connections = []
def __enter__(self):
sock = socket(self.family, self.type)
sock.connect(self.address)
self.connections.append(sock)
return sock
def __exit__(self, exc_ty, exc_val, tb):
self.connections.pop().close()
conn = LazyConnection(('www.python.org', 80))
with conn as s1:
pass
with conn as s2:
pass
在第二个版本中, LazyConnection
类可以被看做是某个连接工厂
。在内部,一个列表
被用来构造一个栈
。每次 __enter__ ()
方法执行的时候,它复制
创建一个新的连接
并将其加入
到栈
里面。 __exit__ ()
方法简单
的从栈中弹出
最后一个连接并关闭它
。这里稍微有点难理解,不过它能允许嵌套使用 with 语句创建多个连接,就如上面演示的那样。
在需要管理一些资源
比如文件
、网络连接
和锁
的编程环境中,使用上下文管理器
是很普遍的。这些资源的一个主要特征
是它们必须被手动的关闭或释放
来确保程序的正确运行。例如,如果你请求了一个锁
,那么你必须确保之后释放了它
,否则就可能产生死锁
。通过实现__enter__ ()
和 __exit__ ()
方法并使用 with
语句可以很容易的避免这些问题,因为 __exit__ ()
方法可以让你无需担心这些了。
方式二:
使用@contextmanager
r 装饰器能减少
创建上下文管理器
的样板代码量
,因为不用编写一个完整的类
,定义 __enter__
和 __exit__
方法,而只需实现有一个 yield
语句的生成器
,生成想让__enter__
方法返回的值
。
在使用 @contextmanager
装饰的生成器中
,yield
语句的作用是把函数的定义体
分成两部分
:yield
语句前面的所有代码
在 with
块开始时(即解释器调用 __enter__
方法时)执行,yield
语句后面的代码
在 with
块结束时(即调用 __exit__
方法时)执行。
# -*- coding: utf-8 -*-
"""
更改输出流
"""
import contextlib
@contextlib.contextmanager # 应用 contextmanager 装饰器。
def looking_glass():
import sys
original_write = sys.stdout.write # 贮存原来的 sys.stdout.write 方法
def reverse_write(text): # 定义自定义的 reverse_write 函数;在闭包中可以访问 original_write
original_write(text[::-1])
sys.stdout.write = reverse_write # 把 sys.stdout.write 替换成 reverse_write
# 产出一个值,这个值会绑定到 with 语句中 as 子句的目标变量上。执行 with 块中的代
# 码时,这个函数会在这一点暂停
yield 'JABBERWOCKY'
# 控制权一旦跳出 with 块,继续执行 yield 语句之后的代码;这里是恢复成原来的 sys.stdout.write 方法。
sys.stdout.write = original_write
if __name__ == '__main__':
with looking_glass() as what:
print('what', what)
# YKCOWREBBAJ
print(what)
# JABBERWOCKY