在之前的示例中我们使用了大量的 和 来管理 Python Object 的引用计数。 的 里也使用了一种非常啰嗦的语法:
这一切都是为了内存管理。
本章我们将回顾 Python 的垃圾回收机制,包括引用计数、循环检测和弱引用这些概念;
在这个基础上,我们再来讨论在设计我们的类的时候要如何做才能避免内存泄漏;
接着,我们给类添加继承和被继承的功能,并展示如何复写特定的属性和函数;
最后,我们给类添加那些锦上添花的高级功能,比如静态属性、静态方法、类方法,以及一些 。
一、Python 的垃圾回收机制
Python 里面每一个对象都用 创建于 heap 上,所以每一个不再需要的对象都必须调用一次且仅一次 来释放内存,否则就会出问题(内存泄露、coredump或者其它未知问题)。
以上事实并不难理解,但是很难落实,因为不好掌握 的时机。依靠人的自觉和记忆力来执行 操作总是不靠谱的。所以很多编程语言都使用了某种自动的垃圾回收策略。 Python 所使用的策略就是引用计数。
1.1 引用计数
每当 Python 创建一个 Object,就会在 heap 中申请一块内存。每个 Object 会记录自己的引用计数,当引用计数减少到 0 的时候,就会 1.触发 Object 的 Destructor 并 2.销毁对象回收内存。
什么时候会引起引用计数的变化呢?在讨论这些情况之前,我们不妨先重申一下对象和变量的概念。
大多时候我们都是使用的变量,但变量不等于对象,变量只是对象的一个引用。 时,我们习惯说 是一个值为 的 ,但更严格地说 只是一个指向 的变量。
我们可以把任何左值看做变量,, 或者 都可以看做变量,因为它们都具有引用某个对象的能力。
每个对象在被创建的时候,默认引用计数是 1。之后每当多一个变量指向引用的时候,我们希望引用计数加 1,每少一个变量指向引用计数的时候,引用计数减 1。只有一种情况例外,那就是新创建的对象第一次被一个变量引用的时候,引用计数不变。否则的话, 就会造成新创建的 对象有两个引用!
于是我们就可以总结出以下引起引用计数变化的情况:
创建对象, 比如 , 对象的引用计数为 1;
通过变量 A 赋值变量 B,比如 把 对象的引用计数从 1 加到 2,而 原来所指的对象的引用计数减 1;
删除变量,比如显式地 或者销毁 从而删除 ,使变量所指对象的引用计数减 1;
把对象作为参数传入函数时引用计数加 1,函数返回后作为参数的引用计数减 1;
1.2 循环检测
在 里面, 包含对 的引用, 又包含对 的引用。所以当函数执行完之后,本应该释放的变量由于循环引用而没有被释放。
为了处理这种问题,Python 使用循环检测(cycle detector) 作为引用计数机制的补充。简单说就是即使 A、B 两个对象互相引用,但是除此之外没有任何对象指向它们中的任何一个时,就可以把它们当做垃圾来回收(我们不讨论标记清除这种垃圾回收机制)。
1.3 弱引用
弱引用的作用是保持对对象的引用的同时又不增加对象的引用计数。比如有时候我们想要持有一份对对象的记录,当对象不再需要时,删除记录。
如果我们直接使用强引用做记录,会造成对象的引用计数加 1,而对象不再被需要时,由于记录自身的存在,引用计数不会减到 0,从而导致内存泄漏。
二、设计对内存资源友好的类
引用计数、循环检测和弱引用是 Python 提供给我们的利器。当我们使用 C 来拓展 Python 的时候,也要实现 Python 所提供的这些便利性。
在这一节,我们将优化 类,顺引用计数的操作逻辑,并使它支持循环检测和弱引用。
2.1 管理引用计数
我们在第一节提到了 4 个改变引用计数的情况,现在我们依次从这四个方面检查我们的 类:
改变引用计数情况1. 创建对象, 比如 , 对象的引用计数为 1
由于新创建的对象默认引用计数是 1,所以只要我们不在 函数和 函数里面 就好了。
改变引用计数情况2. 通过变量 A 赋值变量 B,比如 把 对象的引用计数从 1 加到 2,而 原来所指的对象的引用计数减 1
赋值时涉及到两个引用计数操作,一是原来的对象引用计数要减 1,二是新的对象引用计数加 1。我们再来看看 的 源码:
语句上面的四行代码,就实现了新旧对象的引用计数的增减。
我们也注意到了 变量,为什么要多一步保存旧对象到 呢?为什么不直接写成如下语句:
如果写成这种简单的形式的话,执行第一句之后, 的引用计数可能变成 0 从而触发它的 ,而 会执行什么样的操作我们并不知情,它有可能尝试读取 实例的 (它并不知道自己就是 )并修改某些状态,而这是一个正在被销毁的对象(我想象不到实际的情况,实际上我想了两天也每想到,Python 官方 Doc 也只是稍微提了一下。但确实有这种可能性)。
改变引用计数情况3. 删除变量,比如显式地 或者销毁 从而删除 ,使变量所指对象的引用计数减 1;
我们的 对象在被销毁后,并没有释放其占有的资源。而如果要实现这个效果,我们还需要定义我们的 destructor(dealloc) 函数。
我们的 destructor 把自己所有的成员变量的引用计数都减去 1。由于成员变量有可能为 ,所以我们使用的是 。
然后,我们通过 TypeObject 的 函数来释放自己所占用的资源。
现在我们再来测试一下 类:
改变引用计数情况4. 把对象作为参数传入函数时引用计数加 1,函数返回后作为参数的引用计数减 1;
这个在上面三种情况的每个测试中都有体现,就不再细说。
至此,我们的 类已经能够很好地支持引用计数的特性了。
2.2 支持循环检测
一般来说只有容器类型的类才需要支持循环检测,而 的 并不限制数据类型,因此就显得有必要。
要使 类支持循环检测,我们需要提供一个固定范式的 函数来遍历所有可能涉及到循环引用的成员属性:
以及一个 函数来清除所有可能涉及到循环引用的成员属性:
以上两个函数可以使用宏来简化:
之后,再调整 函数以避免循环 gc:
做好这些准备后,最后再去修改对应的 :
这样,我们的 类就能支持循环检测了。
2.3 支持弱引用
如果现在对我们的 类使用弱引用是会报错的:
接下来我们让 类支持弱引用。概括地说,要使一个类支持弱引用,需要 4 个步骤:
Object() 里面添加一个 作为 weakref list
函数里面把 初始化为
函数里面用 清空 weakref list
Type 里面设置
以下是对 类进行的修改:
其它部分的代码保持不变。我们重新编译和安装后,在 Interpreter 里面测试如下
终于,我们的 类经过引用计数管理、循环检测和弱引用三方面的打磨,已经是一个内存友好、使用方便的类了!
三、类的继承
我们希望我们的类可以被继承,也能继承别的类。
假设我们现在要实现一个功能大体和 类相同的类,唯一的区别是它的 只能是 。
简单的做法是在 Python 中定义一个 类,复写其 。
复杂的做法是在 C 里面定义一个 类的子类,我们自己实现 类对 类的继承细节。
3.1 简单的方法
我们不妨先这么做:
然而我们发现 类竟然不能用作基类来继承。 要想让我们的 类能够被继承,需要给 slot 添加 :
是的,从接口的要求上来说只需要这样就可以了。我们在 Interpreter 中再来测试:
3.2 复杂的方法
如果我们希望复写的函数也用 C 来实现,那么我们可能就要在 C 里面实现整个子类。其主要事项有三:
Object() 第一项不再是 , 改成父类的 ;
设置 slot 为父类的 Type
根据需要复写父类的方法
下面我们来在 C 里面实现 类:
以上就是 在 C 里面的定义。在 Interpreter 里面测试如下:
四、静态属性、静态方法、类属性以及
类最重要的属性都已经实现了,还剩下一些锦上添花的高级属性,我们简要地过一下。
4.1 静态属性
静态属性,或者我们把它称之为类的属性,显然并非定义在 Object() 里面,而应该在 Type 里面。Type 有一个 slot 可以用来保存 Type 的属性,也就是类的属性。
如果我们要给 类添加一个静态属性 来记录实例数量,我们可以这么做:
当然,我们也要在实例的 和 函数里面做对应的修改:
让我们编译后来测试一下:
在以上的测试中,我们发现虽然我们成功添加了 count 这个静态属性,但是它好像没有按预期及时更新,而是需要手动调用 后才更新。这看起来像是某种缓存机制所导致的 bug。
实际上,Python 的确缓存了内部属性的查找结果,所以为了得到正确的结果,每当我们对内部的某个属性进行更改后,应该要删除缓存。怎么删除呢?通过 这个函数。
我们在 和 函数里加入这步操作:
这样我们的 属性就能及时反应正确的数值了。
4.3 类方法
定义类方法和定义成员方法一样,所不同的是第一个参数不再是实例,而是类,为了达到这个目的,我们只需要把函数的参数 flag 中再加上 就好了:
如上我们就定义了一个 的类方法。
4.2 静态方法
定义静态方法和定义成员方法也是一样的,区别在于第一个参数是 。而为了实现这个效果,我们只需要 这个 flag:
4.4 和
Type 有两个 slot 和 ,分别用于 和 。我们定义 和 时,需要注意的只有两点: 1. 返回 str,2. 只接受 self 一个参数:
4.5 比较操作符
我们希望 支持比较的操作, 当 和 都相等时,两个 实例相等。
我们只需要实现比较操作符的函数,并把它插入 即可:
以下是上述几个特性的测试:
五、总结
在 C 里面定义类的步骤,不管你想要实现到多少细节,大体上的行为是分为 3 步的:
定义 Object(data impl)
定义 Type(behavior wrapper)
定义各方面具体的行为和属性,并设置到 Type 的 slot 里面。
由于我们是在用 C 编程,所以需要格外小心内存问题,Python 提供的那些内存便利机制,像引用计数、循环检测和弱引用这些,也要我们自己实现。
我们对类的高级特性的探索就到这里了。对其它更多的细节感兴趣的朋友可以自己再去查看官方文档,或者给我留言讨论。
领取专属 10元无门槛券
私享最新 技术干货