前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python ExitStack的优雅退出

Python ExitStack的优雅退出

原创
作者头像
flavorfan
发布2022-09-30 15:45:37
1.5K0
发布2022-09-30 15:45:37
举报
文章被收录于专栏:范传康的专栏范传康的专栏

我相信 Python 的 ExitStack 功能并没有得到应有的认可。我认为部分原因是它的文档位于(已经晦涩的)contextlib 模块的深处,因为正式的 ExitStack 只是 Python 的 with 语句的许多可用上下文管理器之一。但 ExitStack 值得更突出的关注。

1 引子

最近,在研究Google的aiyprojects-raspbia代码中,发现它大量使用contextlib的ExitStatck()管理资源释放。由此契机,研究了下contextlib相关。

代码语言:javascript
复制
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()方法,确保任何意外情况资源的释放。

2 问题:外部资源的释放

外部资源的主要挑战是必须在不再需要它们时释放它们——特别是在出现错误情况时可能输入的所有替代执行路径中,大多数语言将错误条件实现为可以“捕获”和处理的“异常”(Python、Java、C++),或者作为需要检查以确定是否发生错误的特殊返回值(C、Rust、Go)。通常,需要获取和释放外部资源的代码如下所示:

代码语言:javascript
复制
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,则会变成下面:

代码语言:javascript
复制
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;

这种方法有三个大问题:

  1. 清理代码远离分配代码。
  2. 当资源数量增加时,缩进级别(或跳转标签)会累积,使内容难以阅读。
  3. 以这种方式管理动态数量的资源是不可能的。

在python中,使用with语句可以缓解其中一些问题:

代码语言:javascript
复制
@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

然而,这个解决方案远非最佳:需要实现特定于资源的上下文管理器(请注意,在上面的示例中,我们默默地假设两个资源都可以由同一个函数获取),只有当同时分配所有资源并使用丑陋的延续线(在这种情况下不允许使用括号),您仍然需要提前知道所需资源的数量。

3 ExitStack的强大之处

ExitStack 修复了上述所有问题,并在此基础上增加了一些好处。 ExitStack(顾名思义)是一堆清理函数。向堆栈添加回调。但是,清理函数不会在函数返回时执行,而是在执行离开 with 块时执行 - 直到那时,堆栈也可以再次清空。最后,清理函数本身可能会引发异常,而不会影响其他清理函数的执行。即使多次清理引发异常,您也将获得可用的堆栈跟踪。

show me the code

代码语言:javascript
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 引子
  • 2 问题:外部资源的释放
  • 3 ExitStack的强大之处
    • show me the code
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档