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

阅读数:127 2019 年 11 月 18 日 17:41

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

python 在大家的印象中,没有专用的构造和析构函数。但是,从现在开始,作者将带领大家熟悉 python 中的 __init__ 和 __del__ 函数,以替代构造和析构机制。

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

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

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

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

1 举个栗子

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

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

输出:

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

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

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

如下测试所示:

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

输出:

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

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

2 上下文管理器

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

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

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

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

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

3 析构函数和计数垃圾收集器

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

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

输出

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

emmm…为什么我的 del 不执行呢??

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

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

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

4 解决方案

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

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

1. 程序设计时尽可能减少不合理的循环引用。
2. 资源应由最低级别的对象保存。不要在前台程序中直接保存 DB 资源。使用对象封装 DB 连接并在析构函数中安全地关闭它,DB 对象没有任何理由在代码中保存对其他对象的引用。
3.Dependency injection 可以有效防止复杂代码中的循环引用,但是当您发现自己的确需要真正的循环引用实现业务逻辑时,weakref 模块了解一下。

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

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

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

输出:

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

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

5 结论

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

本文转载自公众号 360 云计算(ID:hulktalk)。

原文链接:

https://mp.weixin.qq.com/s/d2_MTvfbOFZmppej5UOwUw

评论

发布