我相信 Python 的 ExitStack 功能并没有得到应有的认可。我认为部分原因是它的文档位于(已经晦涩的)contextlib 模块的深处,因为正式的 ExitStack 只是 Python 的 with 语句的许多可用上下文管理器之一。但 ExitStack 值得更突出的关注。
最近,在研究Google的aiyprojects-raspbia代码中,发现它大量使用contextlib的ExitStatck()管理资源释放。由此契机,研究了下contextlib相关。
import contextlib
class Board:
"""An interface for the connected AIY board."""
def __init__(self, button_pin=BUTTON_PIN, led_pin=LED_PIN):
# 用于动态管理退出回调堆栈的上下文管理器
self._stack = contextlib.ExitStack()
...
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
def close(self):
# 调用close方法展开上下文堆栈调用退出方法的调用
self._stack.close()
...
@property
def button(self):
"""Returns a :class:`Button` representing the button connected to
the button connector."""
with self._lock:
if not self._button:
# 将其__Exit__方法作为回调压栈,并返回__Enter__方法的结果
self._button = self._stack.enter_context(Button(self._button_pin))
return self._button
Board类使用contextlib.ExitStack()创建了self._stack的堆栈,使用enter_context获得创建所属资源Button、LED等对象外,还把成员对象__Exit方法压栈self.stack,并且__exit__方法调用close()方法,确保任何意外情况资源的释放。
外部资源的主要挑战是必须在不再需要它们时释放它们——特别是在出现错误情况时可能输入的所有替代执行路径中,大多数语言将错误条件实现为可以“捕获”和处理的“异常”(Python、Java、C++),或者作为需要检查以确定是否发生错误的特殊返回值(C、Rust、Go)。通常,需要获取和释放外部资源的代码如下所示:
res1 = acquire_resource_one()
try:
# do stuff with res1
res2 = acquire_resource_two()
try:
# do stuff with res1 and res2
finally:
release_resource(res2)
finally:
release_resource(res1)
如果语言没有exceptions,则会变成下面:
res1 = acquire_resource_one();
if(res == -1) {
retval = -1;
goto error_out1;
}
// do stuff with res1
res2 = acquire_resource_two();
if(res == -1) {
retval = -2;
goto error_out2;
}
// do stuff with res1 and res2
retval = 0; // ok
error_out2:
release_resource(res2);
error_out1:
release_resource(res1);
return retval;
这种方法有三个大问题:
在python中,使用with语句可以缓解其中一些问题:
@contextlib.contextmanager
def my_resource(id_):
res = acquire_resource(id_)
try:
yield res
finally:
release_source(res)
with my_resource(RES_ONE) as res1, \
my_resource(RES_TWO) as res2:
# do stuff with res1
# do stuff with res1 and res2
然而,这个解决方案远非最佳:需要实现特定于资源的上下文管理器(请注意,在上面的示例中,我们默默地假设两个资源都可以由同一个函数获取),只有当同时分配所有资源并使用丑陋的延续线(在这种情况下不允许使用括号),您仍然需要提前知道所需资源的数量。
ExitStack 修复了上述所有问题,并在此基础上增加了一些好处。 ExitStack(顾名思义)是一堆清理函数。向堆栈添加回调。但是,清理函数不会在函数返回时执行,而是在执行离开 with 块时执行 - 直到那时,堆栈也可以再次清空。最后,清理函数本身可能会引发异常,而不会影响其他清理函数的执行。即使多次清理引发异常,您也将获得可用的堆栈跟踪。
import contextlib
import sys
from typing import IO
from typing import Optional
# 使用with
def output_line_v1(
s: str,
stream: IO[str],
*,
filename: Optional[str] = None,
) -> None:
if filename is not None:
with open(filename,'w') as f:
for output_stream in (f, stream):
output_stream.write(f'{s}\n')
else:
stream.write(f'{s}\n')
# 使用contextlib
def output_line_v2(
s: str,
stream: IO[str],
*,
filename: Optional[str] = None,
) -> None:
if filename is not None:
f = open(filename,'w')
streams = [stream, f]
ctx = f
else:
streams = [stream]
ctx = contextlib.nullcontext()
with ctx:
for output_stream in streams:
output_stream.write(f'{s}\n')
# 使用ExitStack()
def output_line_v3(
s: str,
stream: IO[str],
*,
filename: Optional[str] = None,
) -> None:
with contextlib.ExitStack() as ctx:
streams = [stream]
if filename is not None:
streams.append(ctx.enter_context(open(filename,'w')))
for output_stream in streams:
output_stream.write(f'{s}\n')
# output_line_v1('hello world', stream=sys.stdout)
# output_line_v1('googlebye world', stream=sys.stdout, filename='log.log')
# output_line_v2('hello world', stream=sys.stdout)
# output_line_v2('googlebye world', stream=sys.stdout, filename='log.log')
output_line_v3('hello world', stream=sys.stdout)
output_line_v3('googlebye world', stream=sys.stdout, filename='log.log')
这是一个简单的例子,output_line需要处理可变的输入资源,需要把信息同时打印到stream屏幕或者一个文件,当有filename输入时,需要确保它的关闭。分别用了with,contextlib,ExitStack,可以看出,用Exitstack的方法逻辑最清晰,代码简洁,而且可扩展性最佳。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。