引用计数、循环检测和弱引用是 Python 提供给我们的利器。当我们使用 C 来拓展 Python 的时候,也要实现 Python 所提供的这些便利性。
在这一节,我们将优化 Record 类,顺引用计数的操作逻辑,并使它支持循环检测和弱引用。
2.1 管理引用计数
我们在第一节提到了 4 个改变引用计数的情况,现在我们依次从这四个方面检查我们的 Record 类:
改变引用计数情况1. 创建对象, 比如 r = Record(), Record 对象的引用计数为 1
1 2 3 4 5 6 7 8 9
Python 3.5.3 (default, Mar 82018, 14:52:49) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> import sys >>> x = Record("x") >>> sys.getrefcount(x) 2# 把 x 做参数传入函数会使引用计数加 1,所以此时得到 1+1 为正确结果 >>>
由于新创建的对象默认引用计数是 1,所以只要我们不在 new 函数和 init 函数里面 Py_INCREF 就好了。
改变引用计数情况2. 通过变量 A 赋值变量 B,比如 r2 = r 把 Record 对象的引用计数从 1 加到 2,而 r2 原来所指的对象的引用计数减 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Python 3.5.3 (default, Mar 82018, 14:52:49) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> import sys >>> a = Record("a") >>> b = Record("b") >>> c = Record("c") >>> a.value = b >>> sys.getrefcount(a) 2# 加 1+1 后的结果,OK(以后不再解释为什么引用计数多了1) >>> sys.getrefcount(b) 3# a.value 指向了 b,b 的引用计数变为 2, OK >>> a.value = c >>> sys.getrefcount(b),sys.getrefcount(c) (2, 3) # a.value 指向对象 c,b 的引用计数变为减 1 变为 1, c 的引用计数加 1 变为 2, OK
Python 3.5.3 (default, Mar 82018, 14:52:49) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> import sys >>> a = Record("a") >>> b = Record("b") >>> c = a >>> sys.getrefcount(a),sys.getrefcount(b),sys.getrefcount(c) (3, 2, 3) # a 和 c 都指向 Record("a") 对象, OK >>> del c >>> sys.getrefcount(a),sys.getrefcount(b) (2, 2) # 删除 c 后,a 的引用计数变回 2, OK >>> a.value = b >>> sys.getrefcount(a),sys.getrefcount(b) (2, 3) # a.value 指向 b,b 的引用计数加 1, OK >>> del a >>> sys.getrefcount(b) 3# 删除 a 后,a.value 不存在了,然而 b 的引用计数还是 3,有问题!!! >>>
我们的 Record 对象在被销毁后,并没有释放其占有的资源。而如果要实现这个效果,我们还需要定义我们的 destructor(dealloc) 函数。
Python 3.5.3 (default, Mar 82018, 15:56:15) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> import sys >>> a = Record("a") >>> b = Record("b") >>> a.value = b >>> sys.getrefcount(a),sys.getrefcount(b) (2, 3) # OK >>> del a >>> sys.getrefcount(b) 2# a 被销毁,a.value 也被销毁,而 b 的引用计数也从 3 减到 2,正常! >>>
改变引用计数情况4. 把对象作为参数传入函数时引用计数加 1,函数返回后作为参数的引用计数减 1;
这个在上面三种情况的每个测试中都有体现,就不再细说。
至此,我们的 Record 类已经能够很好地支持引用计数的特性了。
2.2 支持循环检测
一般来说只有容器类型的类才需要支持循环检测,而 Record 的 value 并不限制数据类型,因此就显得有必要。
要使 Record 类支持循环检测,我们需要提供一个固定范式的 traverse 函数来遍历所有可能涉及到循环引用的成员属性:
Python 3.5.3 (default, Mar 82018, 15:56:15) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> import weakref >>> from myclass import Record >>> r = Record("r") >>> ref = weakref.ref(r) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: cannot create weak reference to 'myclass.Record'object >>>
接下来我们让 Record 类支持弱引用。概括地说,要使一个类支持弱引用,需要 4 个步骤:
Object(Impl) 里面添加一个 PyObject* 作为 weakref list
new 函数里面把 weakref_list 初始化为 NULL
dealloc 函数里面用 PyObject_ClearWeakRefs 清空 weakref list
Python 3.5.3 (default, Mar 102018, 06:41:24) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> import weakref >>> from myclass import Record >>> r = Record("r") >>> ref = weakref.ref(r) >>> r <myclass.Record object at 0x7ff8f2b8b848> >>> ref <weakref at 0x7ff8f325f5e8; to 'myclass.Record' at 0x7ff8f2b8b848> >>> ref() <myclass.Record object at 0x7ff8f2b8b848> >>> del r >>> ref <weakref at 0x7ff8f325f5e8; dead> >>> ref() # 删除 r 后,弱引用 ref 状态为 dead,调用 ref 返回 None >>>
终于,我们的 Record 类经过引用计数管理、循环检测和弱引用三方面的打磨,已经是一个内存友好、使用方便的类了!
三、类的继承
我们希望我们的类可以被继承,也能继承别的类。
假设我们现在要实现一个功能大体和 Record 类相同的类,唯一的区别是它的 value 只能是 str。
简单的做法是在 Python 中定义一个 StringRecord 类,复写其 value property。
复杂的做法是在 C 里面定义一个 Record 类的子类,我们自己实现 StringRecord 类对 Record 类的继承细节。
# 尝试给 value 设置一个 int 值,预料之中的报错 >>> sr.value = 22 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 8, in value AttributeError: value must be strtype
# value 只能是 str,完美!(啊,好想玩 Switch...) >>> sr.value = "I want to play Switch" >>> sr.print() 'John I want to play Switch'
3.2 复杂的方法
如果我们希望复写的函数也用 C 来实现,那么我们可能就要在 C 里面实现整个子类。其主要事项有三:
>>> from myclass import StringRecord >>> sr = StringRecord() # new 和 init 等方法仍然是继承来的 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Required argument 'name' (pos 1) not found >>> sr = StringRecord(name="John") >>> sr.value = 22# value setter 使用了新的函数 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: The value attribute value must be a string >>> sr.value = "I really want to play Switch!!!" >>> sr.print() 'John I really want to play Switch!!!' >>>
四、静态属性、静态方法、类属性以及 __magic_functions__
Record 类最重要的属性都已经实现了,还剩下一些锦上添花的高级属性,我们简要地过一下。
4.1 静态属性
静态属性,或者我们把它称之为类的属性,显然并非定义在 Object(Impl) 里面,而应该在 Type 里面。Type 有一个 tp_dict slot 可以用来保存 Type 的属性,也就是类的属性。
如果我们要给 Record 类添加一个静态属性 count 来记录实例数量,我们可以这么做:
Python 3.5.3 (default, Mar 102018, 10:19:23) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> help(Record) # Record 类有了一个 count 属性,初始值为 0
Help on classRecordin module myclass:
classRecord(builtins.object) | built-in Record type | | Methods defined here: | | __init__(self, /, *args, **kwargs) | Initialize self. See help(type(self)) for accurate signature. | | __new__(*args, **kwargs) from builtins.type | Create andreturn a new object. See help(type) for accurate signature. | | print(...) | Print name and value | | ---------------------------------------------------------------------- | Data descriptors defined here: | | name | name property | | value | value property | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | count = 0 (END)
static PyObject* record_get_purpose(PyObject * null, PyObject *args) { return Py_BuildValue("s", "Using a pair of name and value to record anything you want"); }
static PyMethodDef record_methods[] = { {"print", (PyCFunction) record_print, METH_NOARGS, "Print name and value"}, {"get_count", (PyCFunction) record_get_count, METH_VARARGS | METH_CLASS, "Get instance count of this class"}, {"get_purpose", (PyCFunction) record_get_purpose, METH_VARARGS | METH_STATIC, "Get the purpose this class"}, {NULL} };
Python 3.5.3 (default, Mar 102018, 12:11:52) [GCC 5.4.020160609] on linux Type"help", "copyright", "credits"or"license"for more information. >>> from myclass import Record >>> Record.get_purpose() 'Using a pair of name and value to record anything you want' >>> a = Record("A") >>> a myclass.Record(name=A, value=None) >>> str(a) 'name: A, value: None' >>> >>> b = Record("B") >>> a == b False >>> c = Record("A") >>> a == c True >>> a.get_count() 3 >>> del a >>> del b >>> del c >>> Record.count 0 >>> Record.get_count() 0 >>>
五、总结
在 C 里面定义类的步骤,不管你想要实现到多少细节,大体上的行为是分为 3 步的:
定义 Object(data impl)
定义 Type(behavior wrapper)
定义各方面具体的行为和属性,并设置到 Type 的 tp_xxx slot 里面。
由于我们是在用 C 编程,所以需要格外小心内存问题,Python 提供的那些内存便利机制,像引用计数、循环检测和弱引用这些,也要我们自己实现。
我们对类的高级特性的探索就到这里了。对其它更多的细节感兴趣的朋友可以自己再去查看官方文档,或者给我留言讨论。