01月18, 2019

关于使用 Python 析构函数的正确姿势

析构函数是 C++ 中一个非常重要的概念,析构函数 (destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。 析构函数往往用来做“清理善后” 的工作,例如在建立对象时用 new 开辟了一片内存空间,delete 则会调用析构函数后释放内存。

而在 Python 中没有专用的构造和析构函数,但是一般可以在__init____del__分别完成初始化和删除操作,以替代构造和析构。

但是 Python 社区中的许多人都不推荐使用 __del__,因为 Python 对对象使用了引用计数来管理,很多情况下是很难以估计是什么时候引用计数为 0 而造成销毁的,同时很多使用技巧告诉我们使用 Python 编程不用再过度优化内存使用,以避免写出 C++ 风格的代码。

在本文中,我们将明确如何来正确使用__del__

0x01 举个栗子

我们先来看个简单的测试用例:

class FooType(object):
    def __init__(self, id):
        self.id = id
        print self.id, 'born'

    def __del__(self):
        print self.id, 'released'

ft = FooType(1)

输出:

1 born
1 released

Python 里也同 Java 一样采用了垃圾收集机制,不过不一样的是:

Python 采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略。因此,当Python在超出范围时不会清理它,只有当它的最后一次引用超出范围时,才会将它清理掉。

如如下测试所示:

class FooType(object):
    def __init__(self, id):
        self.id = id
        print self.id, 'born'

    def __del__(self):
        print self.id, 'released'

def make_foo():
    print 'Making...'
    ft = FooType(2)
    print 'Returning...'
    return ft

print 'Calling...'
ft = make_foo()
print 'End...'

输出:

Calling...
Making...
2 born
Returning...
End...
2 released

见输出最后一行,主体程序结束后才调用 FooType 类 __del__ 方法,而不是当 ft 超出 make_foo 的作用域时就去立即调用 __del__ 方法。

0x02 上下文管理器

刚开始写 Python 时,我很长一段时间都在用 Python 写 C++,直到当我开始逐渐了解习惯并喜欢上使用库以及更多高级概念 (generators, decorators, contexts, etc) 时,我的 Python 编程技巧才得以提升。

Python提供了一种更好的方法上下文来管理资源 contexts。上下文管理器允许你在有需要的时候,精确地分配和释放资源。 使用上下文管理器最广泛的案例就是 with 语句了。例如,处理写入文件时的最佳方法是:

with open('test.txt', 'w') as f:
    f.write('Voulez-vous coucher avec moi ce soir?')

这可以确保当系统中的块文件被正确关闭与退出,即使引发异常,它也会尝试去关闭文件,这就是 with 语句的主要优势。

不过,如果您的业务场景需要封装了某种类型的数据库,该对象必须在结束时提交并关闭。假设对象是某个大型复杂类的成员变量,父对象在不同的方法中需要不时与 DB 对象交互,那么在这里使用 with 是不实际的,我们则需要一个功能完备的析构函数来帮我们完成相关资源的处理及释放。

0x03 析构函数和计数垃圾收集器

为了解决我在上一段中提到的问题,我们要开始考虑如何有效使用__del__析构函数。首先需要面对的就是解决引用计数垃圾收集器同循环引用之间的问题,举个例子:

class FooType(object):
    def __init__(self, id, parent):
        self.id = id
        self.parent = parent
        print 'FooType', self.id, 'born'

    def __del__(self):
        print 'FooType', self.id, 'released'

class BarType(object):
    def __init__(self, id):
        self.id = id
        self.foo = FooType(id, self)
        print 'BarType', self.id, 'born'

    def __del__(self):
        print 'BarType', self.id, 'released'

b = BarType(3)

输出

FooType 3 born
BarType 3 born

emmm...为什么我的 __del__ 不执行呢??

以下是 Python 官方文档在此问题上的说法:

Circular references which are garbage are detected when the option cycle detector is enabled (it’s on by default), but can only be cleaned up if there are no Python-level __del__() methods involved.

官方文档中表明启用周期检测器时会检测到垃圾的循环引用(默认情况下它是打开的),但只有在没有涉及Python __del__() 方法的情况下才能清除。Python 不知道破坏彼此保持循环引用的对象的安全顺序,因此它则不会为这些方法调用析构函数。

0x04 解决方案

首先,我们可以使用 close() 方法来代替析构函数,但是这类方便并不是绝对安全的,不光是因为它们很容易在编码时忘记去正确调用改方法,而且当程序需要抛出异常时,显式调用 close() 方法就会变得非常麻烦。

不过析构函数也是可以在Python中被安全使用的,注意点可以概括为以下几点:

  1. 程序设计时尽可能减少不合理的循环引用;

  2. 资源应由最低级别的对象保存。不要在前台程序中直接保存DB资源。使用对象封装DB连接并在析构函数中安全地关闭它,DB对象没有任何理由在代码中保存对其他对象的引用。

  3. Dependency injection 可以有效防止复杂代码中的循环引用,但是当您发现自己的确需要真正的循环引用实现业务逻辑时,weakref 模块了解一下。

A weak reference to an object is not enough to keep the object alive: when the only remaining references to a referent are weak references, garbage collection is free to destroy the referent and reuse its memory for something else. A primary use for weak references is to implement caches or mappings holding large objects, where it’s desired that a large object not be kept alive solely because it appears in a cache or mapping.

这是用 weakref 重写的前一个例子:

import weakref

class FooType(object):
    def __init__(self, id, parent):
        self.id = id
        self.parent = weakref.ref(parent)
        print 'FooType', self.id, 'born'

    def __del__(self):
        print 'FooType', self.id, 'released'

class BarType(object):
    def __init__(self, id):
        self.id = id
        self.foo = FooType(id, self)
        print 'BarType', self.id, 'born'

    def __del__(self):
        print 'BarType', self.id, 'released'

b = BarType(4)

输出:

FooType 4 born
BarType 4 born
BarType 4 released
FooType 4 released

在这个例子中,我使用 weakref.ref 在构造函数 FooType 中分配父引用。这是一个弱引用,因此它并没有真正创建一个循环。由于 GC 没有看到循环,它会正常回收两个对象。

0x05 结论

Python 通过 __del__ 方法完全可以使用析构方法,并适用于绝大多数的用例。一些不能普遍使用的场景,比如通常代表是不佳程序设计的循环引用。对于一些必须使用合理循环引用的情况,可以使用 weakref 提供的弱引用来破坏循环以保证 __del__ 正确执行。

参考

Dependency_injection - Wikiwand
Python destructor and garbage collection notes
How do I correctly clean up a Python object?
Cleaning up an internal pysqlite connection on object destruction
Safely using destructors in Python

本文链接:https://www.opsdev.cn/post/Safely-using-destructors-in-Python.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。