首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

C/C+拓展 Python 实战(四)——类的高级属性

在之前的示例中我们使用了大量的 和 来管理 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 提供的那些内存便利机制,像引用计数、循环检测和弱引用这些,也要我们自己实现。

我们对类的高级特性的探索就到这里了。对其它更多的细节感兴趣的朋友可以自己再去查看官方文档,或者给我留言讨论。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180310G0UUWY00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券