在Python中安全使用析构函数

作者: Eli Bendersky

原文链接:https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python

本文适用于Python 2.52.6——如果你看到Python 3有任何不同,请让我知道。

C++中,析构函数是一个非常重要的概念,它们是RAIIresource 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.refparent引用赋值。这是一个弱引用,因此它不会真正创建一个环。因此GC看不到环,它销毁了这两个对象。

结论

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

参考文献

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

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/84872318
今日推荐