方式一:
为了让一个对象兼容 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__ () 方法可以让你无需担心这些了。
方式二:
使用@contextmanagerr 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 __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
本文介绍了如何使用上下文管理器协议with语句创建网络连接,包括类实现__enter__和__exit__方法,以及使用@contextmanager装饰器简化代码。同时讨论了如何处理嵌套连接和资源管理的最佳实践。
2348

被折叠的 条评论
为什么被折叠?



