未加星标

[译]在Python中安全使用析构函数

字体大小 | |
[开发(python) 所属分类 开发(python) | 发布者 店小二03 | 时间 2018 | 作者 红领巾 ] 0人收藏点击收藏

作者: Eli Bendersky

本文适用于 python 2.5 与 2.6―― 如果你看到 Python 3 有任何不同,请让我知道。

在 C++ 中,析构函数是一个非常重要的概念,它们是 RAII ( resource acquisition is initialization )的一个基本成分――在抛出异常的程序中,基本上是编写涉及资源分配与释放代码仅有的安全方式。

在 Python 中,析构函数的需求少得多,因为 Python 有进行内存管理的垃圾收集器。不过,虽然内存是最常见的分配资源,它不是唯一的。还有要关闭的套接字与数据库连接,要刷新的文件、缓冲与缓存,以及在一个对象用完时需要释放的另外几种资源。

因此 Python 有析构函数的概念―― __del__ 方法。出于某个原因, Python 社区里的许多人认为 __del__ 是邪恶的,不应该使用。不过,简单 grep 标准库显示,在我们使用且喜欢的类中使用了数以十计的 __del__ ,那么要点在哪里?在本文中,我将尝试澄清它(首先是为我自己),何时应该使用 __del__ ,以及如何使用。

简单的例子代码

首先一个基本例子:

class FooType ( object ):

def __init__ ( self , id ):

self .id = id

print self .id, 'born'

def __del__ ( self ):

print self .id, 'died'

ft = FooType( 1 )

这打印出:

1 born

1 died

现在,回忆由于一个引用计数垃圾收集器的使用, Python 在一个对象退出作用域时,不会清理它。在该对象的最后一个引用退出作用域时,才将清理它。下面是一个展示:

class FooType ( object ):

def __init__ ( self , id ):

self .id = id

print self .id, 'born'

def __del__ ( self ):

print self .id, 'died'

def make_foo ():

print 'Making...'

ft = FooType( 1 )

print 'Returning...'

return ft

print 'Calling...'

ft = make_foo()

print 'End...'

这打印出:

Calling...

Making...

1 born

Returning...

End...

1 died

在程序终止时调用了这个析构函数,不是在 ft 退出 make_foo 里的作用域时。

析构函数的替代品

在我继续之前,一个合适的揭露:对资源的管理, Python 提供了比析构函数更好的方法――上下文( context )。我不会把这变成上下文的一个教程,但你应该熟悉 with 语句,以及可以在内部使用的对象。例如,处理文件写入的最好方法是:

with open ( 'out.txt' , 'w' ) as of:

of.write( '222' )

这确保在退出 with 内部的代码块时,该文件被正确关闭,即使抛出异常。注意这展示了一个标准的上下文管理器。另一个是 threading.lock ,它返回一个非常适合在一个 with 语句中使用的上下文管理器。更多细节,阅读 PEP 343 。

虽然推荐, with 不总是适用的。例如,假设你有一个封装了某种数据库的对象,在该对象生命期结束时,必须提交并关闭该数据库。现在,假定该对象应该是某种大且复杂的类(比如一个 GUI 会话,或者一个 MVC 模型类)的一个成员变量。父亲在别的方法中不时地与该 DB 对象交互,因此使用 with 是不现实的。所需要的是一个起作用的析构函数。

析构函数何处走偏

为了解决我在上一段展示的用例,你可以采用 __del__ 析构函数。不过,知道这不总是工作良好是重要的。引用计数垃圾收集器的死对头是循环引用。下面是一个例子:

class FooType ( object ):

def __init__ ( self , id , parent):

self .id = id

self .parent = parent

print 'Foo' , self .id, 'born'

def __del__ ( self ):

print 'Foo' , self .id, 'died'

class BarType ( object ):

def __init__ ( self , id ):

self .id = id

self .foo = FooType( id , self )

print 'Bar' , self .id, 'born'

def __del__ ( self ):

print 'Bar' , self .id, 'died'

b = BarType( 12 )

输出:

Foo 12 born

Bar 12 born

噢……发生了什么?析构函数在哪里?下面是 Python 文档在这件事上的陈述:

在启用了可选的循环检测器(默认打开)时,检测垃圾的循环引用,但仅在不涉及 Python 层面的 __del__() 方法时,才能被清理。

Python 不知道销毁彼此持有循环引用的对象的安全次序,因此作为一个设计决策,它只是不对这样的方法调用析构函数!

那么,现在怎么办?

因为其缺陷,我们不应该使用析构函数吗?我非常吃惊地看到许多 Python 支持者认为这样,并建议使用显式的 close 方法。但我不同意――显式的 close 方法不那么安全,因为它们容易忘记调用。另外,在发生异常时(在 Python 里,它们随时出现),管理显式关闭变得非常困难且烦人。

我确实认为析构函数可以且应该在 Python 里被安全地使用。带着几分小心,这绝对可能。

首先以及最重要的,注意到合理的循环引用是罕见的。我故意说合理的( justified )――出现循环引用的大量使用是坏的设计以及有漏洞抽象的样本。

作为一个经验规则,资源尽可能由最底层的对象持有。不要在你的 GUI 会话里直接持有一个 DB 资源。使用一个对象封装这个 DB 连接,并在析构函数里安全地关闭它。 DB 对象没有理由持有你代码里其他对象的引用。如果这样――它违反了几个好的设计实践。

有时,在复杂代码中,依赖性注入( dependency injection )有助于防止循环引用,不过即使在你发现需要一个真循环引用的罕见情形里,也存在解决方案。 Python 为此提供了 weakref 模块。文档很快揭示,这正是我们这里所需要的:

一个对象的弱引用不足以保持对象存活:当一个被引用对象仅有的引用是弱引用时,垃圾收集可以自由地销毁这个被引用对象,并为其他对象重用其内存。弱引用的主要使用是实现缓存或持有大对象的映射,其中期望大对象不仅仅因为出现在缓存或映射中,而被保持存活。

下面是用 weakref 重写的前面的例子:

import weakref

class FooType ( object ):

def __init__ ( self , id , parent):

self .id = id

self .parent = weakref.ref(parent)

print 'Foo' , self .id, 'born'

def __del__ ( self ):

print 'Foo' , self .id, 'died'

class BarType ( object ):

def __init__ ( self , id ):

self .id = id

self .foo = FooType( id , self )

print 'Bar' , self .id, 'born'

def __del__ ( self ):

print 'Bar' , self .id, 'died'

b = BarType( 12 )

现在我们得到希望的结果:

Foo 12 born

Bar 12 born

Bar 12 died

Foo 12 died

这个例子里的小改动是,在 FooType 构造函数里,我使用 weakref.ref 对 parent 引用赋值。这是一个弱引用,因此它不会真正创建一个环。因此 GC 看不到环,它销毁了这两个对象。

结论

Python 有经由 __del__ 方法的完美、可用的对象析构函数。对绝大多数用例,它工作良好,但堵塞在循环引用处。不过,循环引用通常是坏设计的一个迹象,它们很少是合理的。对极少数使用了合理的循环引用的用例里,使用弱引用很容易打破循环, Python 在 weakref 模块里提供弱引用。

参考文献

在准备本文时,某些有用的链接:

Python destructor and garbage collection notes RAII The Python documentation This and also this Stack Overflow discussions.

本文开发(python)相关术语:python基础教程 python多线程 web开发工程师 软件开发工程师 软件开发流程

分页:12
转载请注明
本文标题:[译]在Python中安全使用析构函数
本站链接:https://www.codesec.net/view/620770.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 开发(python) | 评论(0) | 阅读(16)