论Python的ExitStack之美

2020-11-10 06:31:14

我认为,Python的ExitStack功能没有得到应有的认可。我认为部分原因是因为它的文档位于(已经很模糊的)contextlib模块的深处,因为正式的ExitStack只是Python的with语句的众多可用上下文管理器之一。但ExitStack值得关注的远不止这些。这篇文章有望对此有所帮助。

那么,是什么让ExitStack如此重要呢?简而言之,这是在Python中处理外部资源分配和释放的最佳方式。

外部资源的主要挑战是,当您不再需要它们时,您必须释放它们--尤其是,您不能忘记在出现错误的情况下可能输入的所有备用执行路径中这样做。

大多数语言将错误条件实现为异常,可以捕获并处理(Python、Java、C++),或者作为需要检查以确定是否发生错误(C、Rust、Go)的特殊返回值。通常,需要获取和释放外部资源的代码如下所示:

Res1=Acquisition_resource_one()try:#用res1做数据res2=Acquisition_resource_Two()try:#用res1和res2做数据最后:Release_resource(Res2)最后:Release_resource(Res1)。

Res1=Acquisition_resource_one();if(res==-1){retval=-1;goto error_out1;}//使用res1填充res2=获取资源_2();if(res==-1){retval=-2;goto error_out2;}//使用res1和res2填充retval=0;//ok error_out2:Release_resource(Res2);error_out1:Release_resource(Res1);返回。

当资源数量增加时,缩进级别(或跳转标签)会累积,使内容难以阅读。

@contextlib.contextmanager def my_resource(Id_):res=Acquisition_resource(Id_)try:放弃res:Release_source(Res),my_resource(Res_One)为res1,\my_resource(Res_Two)为res2:#用res1做东西#用res1和res2做东西

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

在无异常编程语言的世界里(没有恶意),Go开发了一种不同的补救方法:DEFER语句将表达式的执行推迟到封闭函数返回。使用DEFER,上面的示例可以写成:

Res1=Acquisition_resource_one()if(res==NULL){return-1}推迟RELEASE_RESOURCE(Res1)//使用res1填充res2=Acquisition_resource_Two()if(res==NULL){return-2}推迟RELEASE_RESOURCE(Res2)//使用res1执行填充,res2返回0。

这很好:分配和清理保持在一起,不需要额外的缩进或跳转标签,并且将其转换为动态获取多个资源的循环将非常简单。但仍有一些缺点:

要控制一组资源的确切释放时间,必须将访问相应资源的代码的所有部分分解到单独的函数中。

您不能取消延迟表达式,因此无法取消。如果没有发生错误,则将资源返回给调用方。

ExitStack修复了所有上述问题,并在此基础上增加了一些好处。ExitStack(顾名思义)是一堆清理函数。向堆栈添加回调相当于CallingGo;的DEFER语句。然而,清理函数不会在函数返回时执行,但当执行离开With块时-在此之前,堆栈也可以再次清空。

最后,清理函数本身可能会引发异常,而不会影响其他清理函数的执行。即使多重清理引发异常,您也会得到可用的堆栈跟踪。

其中ExitStack()为cm:res1=Acquisition_resource_one()cm。回调(RELEASE_RESOURCE,res1)#使用res1 res2=Acquisition_resource_Two()cm进行操作。回调(RELEASE_RESOURCE,res2)#使用res1和res2执行操作。

该模式很容易扩展到许多资源(包括在循环中获取的动态数字)。

ExitStack()为cm:res1=cm。输入(open(';first_file';,';r';))#用res1 res2=cm做东西。输入(open(';Second_file';,';r';))#用res1和res2做东西。

要打开一组文件并将其返回给调用方(如果后续打开失败,则不会泄漏已打开的文件):

定义OPEN_FILES(文件列表):FHS=[],将ExitStack()设置为cm:作为文件列表中的名称:FHS。追加(厘米。输入(open(name,';r';)cm。POP_ALL()返回FHS