欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Cython 系列 4.定义和使用 Cython 中的扩展类,分析它们与 Python 中普通类的区别。

最编程 2024-05-26 17:25:48
...

楔子

上一篇博客中,我们介绍 Cython 给 Python 赋予的一些额外特性,以及这些特性的使用方式,但那主要是基本的数据类型和函数。Cython 也可以增强 Python 的类,不过在了解细节之前,我们必须首先了解 Python 类和扩展类之间的区别,这样我们才能明白 Cython 增强 Python 类的做法是什么,以及它为什么要这么做。

Python 类和扩展类之间的差异

首先 Python 中 "一切皆对象",怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,我们通过 id 函数可以获取地址并将每一个对象都区分开来,使用 type 获取类型。Python 中对象有很多属性,这些属性都放在自身的属性字典里面,这个字典可以通过 __dict__ 获取。我们调用对象的某一个属性的时候,可以通过 . 的方式来调用,Python 也允许我们通过 class 关键字自定义一个类。

class A:
    pass


print(A.__name__)  # A
A.__name__ = "B"
print(A.__name__)  # B

try:
    int.__name__ = "INT"
except Exception as e:
    # 内建类型 和 扩展类型 不允许修改属性
    print(e)  # can't set attributes of built-in/extension type 'int'

正如之前说的那样,我们除了在 Python 中定义类,还可以直接使用 Python/C API 在 C 级别创建自己的类型,这样的类型称之为扩展类、或者扩展类型(说白了在 C 中实现的类就叫做扩展类)。

Python 解释器本来就是 C 写的,所以我们可以在 C 的层面上面实现 Python 的任何对象,类也是如此。Python 中自定义的类和内置的类在 C 一级的结构是一致的,所以我们只需要按照 Python/C API 提供的标准来编写即可。但还是那句话,使用 C 来编写会比较麻烦,因为本质上就是写 C 语言。

当我们操作扩展类的时候,我们操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的 C 一级的访问,这种访问可以显著的提高性能。但是在扩展类的实现、以及处理相应的实例对象和在纯 Python 中定义类是完全不同的,需要有专业的 Python/C API 的知识,不适合新手。

这也是 Cython 要增强 Python 类的原因:Cython 使得我们创建和操作扩展类就像操作 Python 中的类一样。在Cython中定义一个扩展类通过 cdef class 的形式,和 Python 中的常规类保持了高度的相似性。

尽管在语法上有着相似之处,但是 cdef class 定义的类对所有方法和数据都有快速的 C 级别的访问,这也是和扩展类和 Python 中的普通类之间的一个最显著的区别。而且扩展类和 int、str、list 等内置的类都属于静态类,它们的属性是不可修改的。

Cython 中的扩展类

写一个 Python 中的类吧。

class Rectangle:
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
        
    def get_area(self):
        return self.width * self.height

这个类是在 Python 级别定义的,可以被 CPython 编译的。我们定义了矩形的宽和高,并且提供了一个方法,计算面积。这个类是可以动态修改的,我们可以指定任意的属性。

如果我们是对这个 Python 类编译的话,那么得到的类依旧是一个纯 Python 类,而不是扩展类。所有的操作,仍然是通过动态调度通用的 Python 对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的 Python 代码仍然需要在运行时动态调度来解析类型。

改成扩展类的话,我们需要这么做。

# cython_test.pyx
cdef class Rectangle:

    cdef long width, height

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def get_area(self):
        return self.width * self.height

此时的关键字我们使用的是 cdef class,意思就是表示这个类不是一个普通的 Python 类,而是一个扩展类。并且在内部,我们还多了一个 cdef long width, height,它负责指定实例 self 所拥有的属性,因为静态类实例不像动态类实例一样可以*添加属性,静态类实例有哪些属性需要在类中使用 cdef 事先指定好。这里的 cdef long width, height 就表示 Rectangle 实例只能有 width 和 height 两个属性、并且类型是 long,因此我们在实例化的时候,参数 w、h 只能传递整数。另外对于 cdef 来说,定义的类是可以被外部访问的,虽然函数不行、但类可以。

import pyximport
pyximport.install(language_level=3)

import cython_test
rect = cython_test.Rectangle(3, 4)
print(rect.get_area())  # 12

try:
    rect = cython_test.Rectangle("3", "4")
except TypeError as e:
    print(e)  # an integer is required

注意:我们在 __init__ 中实例化的属性,都必须在类中使用 cdef 声明,举个栗子。

cdef class Rectangle:
	# 这里我们只声明了width, 没有声明height, 那么是不是意味着这个height可以接收任意对象呢?
    cdef long width

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def get_area(self):
        return self.width * self.height
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
"""
File "cython_test.pyx", line 7, in cython_test.Rectangle.__init__
    self.height = h
AttributeError: 'cython_test.Rectangle' object has no attribute 'height'
"""

凡是在没有在 cdef 中声明的,都不可以赋值给 self,可能有人发现了这不是访问,而是添加呀。我添加一个属性咋啦,没咋,无论是获取还是赋值,self 中的属性必须使用 cdef 在类中声明。我们举一个Python 内置类型的例子吧:

a = 1
try:
    a.xx = 123
except Exception as e:
    print(e)  # 'int' object has no attribute 'xx'

一样等价,我们的扩展类和内建的类是同级别的,一个属性如果想通过 self. 的方式来调用,那么一定要在类里面通过 cdef 声明。

cdef class Rectangle:
    cdef long width, height

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def get_area(self):
        return self.width * self.height
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)

try:
    rect.a = "xx"
except AttributeError as e:
    print(e)  # 'cython_test.Rectangle' object has no attribute 'a'
"""
如果想动态修改、添加属性,那么需要解释器在解释的时候来动态操作
但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力
也正因为如此,才能提高效率。因为很多时候,我们不需要动态修改。
当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里
其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr 等价于instance.__dict__["attr"],同理修改、创建也是。
但是注意:这只是针对普通的 Python 类而言,但扩展类的实例对象内部是没有 __dict__ 的。
"""

try:
    rect.__dict__
except AttributeError as e:
    print(e)  # 'cython_test.Rectangle' object has no attribute '__dict__'

# 不光 __dict__, 你连 self 本身的属性都无法访问
try:
    rect.width
except AttributeError as e:
    print(e)  # 'cython_test.Rectangle' object has no attribute 'width'
# 提示我们 self 没有 width 属性,所以我们实例化之后再想修改是不行的,连获取都获取不到
# 只能调用它的一些方法罢了。

所以内建的类和扩展类是完全类似的,其实例对象都没有属性字典,至于类本身是有属性字典的,但是这个字典不可修改。因为虽然叫属性字典,但它的类型实际上是一个 mappingproxy。

import pyximport
pyximport.install(language_level=3)

import cython_test

try:
    int.__dict__["a"] = 123
except TypeError as e:
    print(e)  # 'mappingproxy' object does not support item assignment
 
try:
    cython_test.Rectangle.__dict__["a"] = 123
except TypeError as e:
    print(e)  # 'mappingproxy' object does not support item assignment

还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候允许你做一些这样的骚操作。但是内置的类和扩展类不需要解释这一步,它们是彪悍的人生,直接指向了 C 一级的数据结构,因此也就丧失了这种动态的能力。

但是扩展类毕竟是我们自己指定的,如果我们就是想修改 self 的一些属性呢?答案是将其暴露给外界即可。

cdef class Rectangle:
    # 通过cdef public的方式进行声明即可
    # 这样的话就会暴露给外界了
    cdef public long width, height

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def get_area(self):
        return self.width * self.height
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
print(rect.width)  # 3
print(rect.get_area())  # 12

rect.width = 123
print(rect.get_area())  # 492

try:
    rect.__dict__
except AttributeError as e:
    print(e)  # 'cython_test.Rectangle' object has no attribute '__dict__'
# 属性字典依旧是没有的

通过 cdef public 声明的属性,是可以被外界获取并修改的,除了 cdef public 之外还有 cdef readonly,同样会将属性暴露给外界,但是只能访问不能修改。

cdef class Rectangle:

    cdef readonly long width, height

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def get_area(self):
        return self.width * self.height
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
print(rect.width)  # 3

try:
    rect.width = 123
except AttributeError as e:
    print(e)  # attribute 'width' of 'cython_test.Rectangle' objects is not writable
  • cdef readonly 类型 变量名:实例属性可以被外界访问,但是不可以被修改
  • cdef public 类型 变量名:实例属性即可以被外界访问,也可以被修改
  • cdef 类型 变量名:实例属性既不可以被外界访问,更不可以被修改

当然定义变量无论是使用 cdef public 还是 cdef readonly,如果是在 Cython 里面实例化的话,内部实例属性在任何情况下都是可以*访问和修改的。因为 Cython 内部会屏蔽扩展类中的 readonly 和 public 的声明,它们存在的目的只是为了控制来自外界(Python)的访问。

这里还有一点需要注意,当在类里面使用 cdef 声明变量的时候,其属性就已经绑定在 self 中了。我们举个栗子:

cdef class Rectangle:

    cdef public long width, height
    cdef public float area
    cdef public list lst
    cdef public tuple tpl
    cdef public dict d
import pyximport

pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle()
print(rect.width)  # 0
print(rect.height)  # 0
print(rect.area)  # 0.0
print(rect.lst)  # None
print(rect.tpl)  # None
print(rect.d)  # None

即便我们没有定义初始化函数,这些属性也是可以访问的,因为在使用 cdef 声明的时候,它们就已经绑定在上面了,而这一步显然发生在编译阶段,只不过这些属性对应的值都是零值。所以 self.xxx = ... 相当于是为绑定在 self 上的属性重新赋值,但赋值的前提是 xxx 必须已经是 self 的一个属性,否则是没办法赋值的,而 xxx 如果想成为 self 的一个属性那么就必须在类里面使用 cdef 进行声明。

但是问题来了,这毕竟是在类里面声明的,那么类是否可以访问呢?

import pyximport

pyximport.install(language_level=3)

import cython_test

print(cython_test.Rectangle.width)  # <attribute 'width' of 'cython_test.Rectangle' objects>

答案是可以访问,不过类访问没有太大意义,打印的结果只是告诉你这是实例的一个属性。如果想设置类属性,就不需要使用 cdef,而是像动态类一样去定义类属性。并且在类里面使用 cdef 的声明属性的时候不可以赋初始值,否则报错,赋值这一步应该在初始化函数中完成。但不使用 cdef、而是像动态类一样定义常规类属性的话,是可以赋初始值的(这是显然的,否则就出现 NameError了)。

C 一级的构造函数和析构函数

每一个实例对象都对应了一个 C 结构体,其指针就是 Python 调用 __init__ 函数里面的 self 参数。当 __init__ 参数被调用时,会初始化 self 参数上的属性,而且 __init__ 参数是自动调用的。但是我们知道在 __init__ 参数调用之前,会先调用 __new__ 方法, __new__ 方法的作用就是为创建的实例对象开辟一份内存,然后返回其指针并交给 self。在 C 级别就是,在调用 __init__ 之前,实例对象指向的结构体必须已经分配好内存,并且所有结构字段都处于可以接收初始值的有效状态。

Cython 扩充了一个名为 __cinit__ 的特殊方法,用于执行 C 级别的内存分配和初始化。不过对于之前定义的 Rectangle 类的 __init__ 方法,因为内部的字段接收的值是两个 double,不需要 C 级别的内存分配。但如果需要 C 级别的内存分配,那么就不可以使用 __init__ 了,而是需要使用 __cinit__。

# 导入相关函数,malloc,free
# 如果不熟悉的话,建议去了解一下C语言
from libc.stdlib cimport malloc, free


cdef class A:
    cdef:
        unsigned int n
        double *array  # 一个数组,存储了double类型的变量

    def __cinit__(self, n):
        self.n = n
        # 在C一级进行动态分配内存
        self.array = <double *>malloc(n * sizeof(double))
        if self.array == NULL:
            raise MemoryError()

    def __dealloc__(self):
        """如果进行了动态内存分配,也就是定义了 __cinit__,那么必须要定义 __dealloc__
        否则在编译的时候会抛出异常:Storing unsafe C derivative of temporary Python reference
        然后我们释放掉指针指向的内存
        """
        if self.array != NULL:
            free(self.array)

    def set_value(self):
        cdef long i
        for i in range(self.n):
            self.array[i] = (i + 1) * 2

    def get_value(self):
        cdef long i
        for i in range(self.n):
            print(self.array[i])
import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(5)
a.set_value()
a.get_value()
"""
2.0
4.0
6.0
8.0
10.0
"""

所以 __cinit__ 是用来进行 C 一级内存的动态分配的,另外我们说如果在 __cinit__ 通过 malloc 进行了内存分配,那么必须要定义 __dealloc__ 函数将指针指向的内存释放掉。当然即使我们不释放也没关系,只不过可能发生内存泄露(雾),但是 __dealloc__ 这个函数是必须要被定义,它会在实例对象回收时被调用。

这个时候可能有人好奇了,那么 __cinit__ 和 __init__ 函数有什么区别呢?区别还是蛮多的,我们细细道来。

首先它们只能通过 def 来定义,另外在不涉及 malloc 动态分配内存的时候, __cinit__ 和 __init__ 是等价的。然而一旦涉及到 malloc,那么动态分配内存只能在 __cinit__ 中进行,如果这个过程写在了 __init__ 函数中,比如将我们上面例子的 __cinit__ 改为 __init__ 的话,你会发现 self 的所有变量都没有设置进去、或者说设置失败,并且其它的方法若是引用了 self.array,那么还会导致丑陋的段错误。

还有一点就是,__cinit__ 函数会在 __init__ 函数之前调用,我们实例化一个扩展类的时候,参数会先传递给 __cinit__,然后 __cinit__ 再将接收到的参数原封不动的传递给 __init__。

cdef class A:
    cdef public:
        unsigned int a, b

    def __cinit__(self, a, b):
        print("__cinit__")
        self.a = a
        self.b = b
        print(self.a, self.b)

    def __init__(self, c, d):
        """__cinit__ 中接收两个参数
        然后会将参数原封不动的传递到这里,所以这里也要接收两个参数
        参数名可以不一致,但是个数要匹配
        """
        print("__init__")
        print(c, d)
import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(111, 222)
"""
__cinit__
111 222
__init__
111 222
"""
print(a.a)  # 111
print(a.b)  # 222

注意:__cinit__ 只有在涉及 C 级别内存分配的时候才会出现,如果没有涉及那么使用 __init__ 就可以,虽然在不涉及 malloc 的时候这两者是等价的,但是 __cinit__ 会比 __init__ 的开销要大一些。而如果涉及 C 级别内存分配,那么建议 __cinit__ 只负责内存的动态分配,__init__ 负责属性的创建。

from libc.stdlib cimport malloc, free


cdef class A:

    cdef public:
        unsigned int a, b, c
    # 这里的 array 不可以使用 public 或者 readonly
    # 原因很简单,因为一旦指定了 public 和 readonly,就意味着这些属性是可以被 Python 访问的
    # 所以需要其能够转化为 Python 中的对象,而 C 中的指针,除了 char *, 都是不能转化为 Python 对象的
    # 因此这里的 array 一定不能暴露给外界,否则编译出错,提示我们:double * 无法转为 Python 对象
    cdef double *array

    def __cinit__(self, *args, **kwargs):
        # 这里面只做内存分配,设置属性交给__init__
        self.array = <int *>malloc(3 * sizeof(int))

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c 
    
    def __dealloc__(self):
        free(self.array)

我们上面使用了 malloc 函数进行内存动态申请、free 函数进行内存释放,但是相比 malloc、free 这种 C 级别的函数,Python 提供了更受欢迎的用于内存管理的函数,这些函数对较小的内存块进行了优化,通过避免昂贵的操作系统调用来加快分配速度。

from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free


cdef class AllocMemory:

    cdef double *data

    def __cinit__(self, size_t number):
        # 等价于 C 的 malloc
        self.data = <double *> PyMem_Malloc(sizeof(double) * number)
        if self.data == NULL:
            raise MemoryError("内存不足,分配失败")
        print(f"分配了 {sizeof(double) * number} 字节的内存")

    def resize(self, size_t new_number):
        # 等价于 C 的 realloc,一般是容量不够了才会使用
        # 相当于是申请一份更大的内存,然后将原来的 self.data 里面的内容拷过去
        # 如果申请的内存比之前还小,那么内容会发生截断
        mem = <double *> PyMem_Realloc(self.data, sizeof(double) * new_number)
        if mem == NULL:
            raise MemoryError("内存不足,分配失败")
        self.data = mem
        print(f"重新分配了 {sizeof(double) * new_number} 字节的内存")

    def __dealloc__(self):
        """定义了 __cinit__,那么必须定义 __dealloc__"""
        if self.data != NULL:
            PyMem_Free(self.data)
        print("内存被释放")

Python 提供的这些内存分配、释放的函数和 C 提供的原生函数,两者的使用方式是一致的,事实上 PyMem_* 系列函数只是在 C 的 malloc、realloc、free 基础上做了一些简单的封装。但不管是哪种,一旦分配了,那么就必须要进行释放,否则只有等到 Python 进程退出之后它们才会被释放,这种情况便称之为内存泄漏。

import pyximport
pyximport.install(language_level=3)

import cython_test

alloc_memory = cython_test.AllocMemory(50)
alloc_memory.resize(60)
del alloc_memory
print("--------------------")
"""
分配了 400 字节的内存
重新分配了 480 字节的内存
内存被释放
--------------------
"""

我们看到是没有任何问题的,因此以后在涉及动态内存分配的时候,建议以后在工作中使用 PyMem_* 系列函数。当然后面为了演示方便,我们还是使用 malloc 和 free。

cdef和cpdef方法

我们之前使用了 cdef 和 cpdef,我们说:cdef 可以定义变量和函数,但是不能被 Python 直接访问;可以定义一个类,能直接被外界访问。而 cpdef 专门用于定义函数,cpdef 定义的函数既可以在 Cython 内部访问,也可以被外界访问,因为它定义了两个版本的函数:一个是高性能的纯C版本(此时等价于 cdef,至于为什么高效,因为它是 C 一级的,直接指向了具体数据结构,当然还有其它原因,我们之前都说过的),另一个是 Python 包装器(相当于我们手动定义的 Python 函数),所以我们还要求使用cpdef定义的函数的参数和返回值类型必须是 Python 可以表示的,像 char * 之外的指针就不行。

那么同理它们也可以作用于方法,当然方法也是实例对象在获取函数的时候进行封装得到的,所以一样的道理。但是注意:cdef 和 cpdef 只能修饰 cdef class 定义的静态类里面的方法,如果是 class 定义的纯 Python 类,那么内部是不可以出现 cdef 或者 cpdef 的。

cdef class A:

    cdef public:
        long a, b

    def __init__(self, a, b):
        self.a = a
        self.b = b

    cdef long f1(self):
        return self.a * self.b

    cpdef long f2(self):
        return self.a * self.b
import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(11, 22)
print(a.f2())  # 242
a.f1()
"""
    a.f1()
AttributeError: 'cython_test.A' object has no attribute 'f1'
"""

cdef 和 cpdef 之间在函数上的差异,在方法中得到了同样的体现。

此外,这个类的实例也可以作为函数的参数,这个是肯定的。

cdef class A:

    cdef public:
        long a, b

    def __init__(self, a, b):
        self.a = a
        self.b = b

    cpdef long f2(self):
        return self.a * self.b
    

def func(self_lst):
    s = 0
    for self in self_lst:
        s += self.f2()
    return s
import pyximport
pyximport.install(language_level=3)

import cython_test

a1 = cython_test.A(1, 2)
a2 = cython_test.A(2, 4)
a3 = cython_test.A(2, 3)
print(cython_test.func([a1, a2, a3]))  # 16

这是 Python 的特性,一切都是对象,尽管没有指明 self_lst 是什么类型,但只要它可以被 for 循环即可;尽管没有指明 self_lst 里面的元素是什么类型,只要它有 f2 方法即可。并且这里的 func 可以在 Cython 中定义,同样可以在 Python 中定义,这两者是没有差别的,因为都是 Python 中的函数。另外在遍历的时候仍然需要确定这个列表里面的元素是什么,意味着列表里面的元素仍然是 PyObject *,它需要获取类型、转化、属性查找,因为 Cython 不知道类型是什么、导致其无法优化。但如果我们规定了类型,那么再调用 f2 的时候,那么会直接指向 C 一级的数据结构,因此不需要那些无用的检测。

cdef class A:

    cdef public:
        long a, b

    def __init__(self, a, b):
        self.a = a
        self.b = b

    cpdef long f2(self):
        return self.a * self.b
    

# 规定接收一个 list,返回一个 long, 它们都是静态的,总之静态类型定义越多速度会越快
cpdef long func(list self_lst):
    # 声明 long 类型的 s,A 类型的 self
    # 我们下面使用的是 s = s + self.f2(), 所以这里的s要赋一个初始值0
    cdef long s = 0
    cdef A self
    for self in self_lst:
        s += self.f2()
    return s

调用得到的结果是一样的,可以自己尝试一下。这样的话速度会变快很多,因为我们在循环的时候,规定了变量类型,并且求和也是一个只使用 C 的操作,因为 s 是一个 double。

这个版本的速度比之前快了 10 倍,这表明类型化比非类型化要快了 10 倍。如果我们删除了 cdef A self,也就是不规定其类型,而还是按照 Python 的语义来调用,那么速度仍然和之前一样,即便使用 cpdef 定义。所以重点在于指定类型为静态类型,只要规定好类型,那么就可以提升速度;而 Cython 是为 Python 服务的,肯定要经常使用 Python 的类型,那么提前规定好、让其指向 C 一级的数据结构,速度会提升很多。如果是 int 和 float,那么会自动采用 C 中的 int 和 float,当然怕溢出的话就使用 long、size_t、ssize_t、double,这样速度就更加快速了。因此重点是一定要静态定义类型,只要类型明确那么就能进行大量的优化。

Python 慢有很多原因,其中一个原因就是它无法对类型进行优化,以及对象分配在堆上。无法基于类型进行优化,就意味着每次都要进行大量的检测,当然这些我们前面已经说过了,如果规定好类型,那么就不用兜那么大圈子了;而对象分配在堆上这是无法避免的,只要你用 Python 的对象,都是分配在堆上,所以对于整型和浮点型,我们通过定义为 C 的类型使其分配在栈上,能够更加的提升速度。总之记住一句话:Cython 加速的关键就在于,类型的静态声明,以及对整数和浮点使用 C 中 long 和 double。

方法中给参数指定类型

无论是 def、cdef、cpdef,都可以给参数规定类型,如果类型传递的不对就会报错。比如:上面的 func 函数如果是普通的 Python 函数,那么内部的参数对于 Python 而言只要能够被 for 循环即可,所以它可以是列表、元组、集合。但是我们上面的 func 规定了类型,参数只能传递 list 对象或者其子类的实例对象,如果传递 tuple 对象就会报错。

然后我们来看看 __init__。

cdef class A:

    cdef public:
        long a, b

    def __init__(self, float a, float b):
        self.a = a
        self.b = b

这里我们规定了类型,但是有没有发现什么问题呢?这里我们的参数 a 和 b 必须是一个 float,如果传递的是其它类型会报错,但是赋值的时候 self.a 和 self.b 又需要接收一个 long,所以这是一个自相矛盾的死结,在编译的时候就会报错。所以给 __init__ 参数传递的值的类型要和类中 cdef 声明的类型保持一致。

即使在类里面,cpdef 仍然不支持闭包。

然后为了更好地解释 Cython 带来的性能改进,我们需要了解关于继承、子类化、和扩展类型的多态性的基础知识。

类的继承与装饰

扩展类型只能继承单个基类,并且继承的基类必须是直接指向 C 实现的类型(可以是使用 cdef class 定义的扩展类型,也可以是内置类型,因为内置类型也是直接指向 C 一级的结构)。如果基类是常规的 Python 类(需要在运行时经过解释器动态解释才能指向 C 一级的结构),或者继承了多个基类,那么 Cython 在编译时会抛出异常。

cdef class Girl:
    cdef public:
        str name
        long age

    def __init__(self, name, age):
        self.name = name
        self.age = age

    cpdef str get_info(self):
        return f"name: {self.name}, age: {self.age}"


cdef class CGirl(Girl):

    cdef public str where

    def __init__(self, name, age, where):
        self.where = where
        super().__init__(name, age)


class PyGirl(Girl):

    def __init__(self, name, age, where):
        self.where = where
        super().__init__(name, age)

我们定义了一个扩展类(Girl),然后让另一个扩展类(CGirl)和普通的 Python 类(PyGirl)都去继承它。我们说扩展类不可以继承 Python 类,但 Python 类是可以继承扩展类的。

import pyximport
pyximport.install(language_level=3)

import cython_test

c_girl = cython_test.CGirl("古明地觉", 17, "东方地灵殿")
print(c_girl.get_info())  # name: 古明地觉, age: 17

py_girl  = cython_test.PyGirl("古明地觉", 17, "东方地灵殿")
print(py_girl .get_info())  # name: 古明地觉, age: 17

print(c_girl.where)  # 东方地灵殿
print(py_girl.where)  # 东方地灵殿

我们看到,对于扩展类和普通的 Python 类,它们都是可以继承扩展类的。

私有属性和私有方法

但是继承的话,子类是否可以访问父类的所有属性或方法呢?我们说cdef定义的方法和函数一样,无法被外部的Python访问,那么内部的 Python 类在继承的时候可不可以访问呢?以及私有属性(方法)的访问又是什么情况呢?

我们先来看看 Python 中关于私有属性的例子。

import pyximport
pyximport.install(language_level=3)

class A:

    def __init__(self):
        self.__name = "xxx"

    def __foo(self):
        return self.__name


try:
    A().__name
except Exception as e:
    print(e)

try:
    A().__foo()
except Exception as e:
    print(e)
"""
'A' object has no attribute '__name'
'A' object has no attribute '__foo'
"""

print(A()._A__name)  # xxx
print(A()._A__foo())  # xxx

我们说定义的私有属性只能在当前类里面使用,一旦出去了就不能够再访问了。其实私有属性(方法)本质上只是 Python 给你改了个名字,在原来的名字前面加上一个 _类名,所以 __name 和 __foo 其实相当于是 _A__name 和 _A__foo。但是当我们在外部用实例去获取 __name 和 __foo 的时候,获取的就是 __name 和 __foo,而显然 A 里面没有这两个属性或方法,因此报错。解决的办法就是使用 _A__name 和 _A__foo,但是不建议这么做,因为这是私有变量,如果非要访问的话,那就不要定义成私有的。如果是在 A 这个类里面获取的话,那么 Python 解释器也会自动为我们加上 _类名 这个前缀,比如我们在类里面获取 self.__name 的时候,实际上获取的也是 self._A__name,但是在外部就不会了。

_A__name = "古明地觉"


class A:

    def __init__(self):
        self.name = __name

# 是不是很神奇呢? 因为在类里面, __name 等价于 _A__name
print(A().name)  # 古明地觉

如果是继承的话,会有什么结果呢?

class A:

    def __init__(self):
        self.__name = "xxx"

    def __foo(self):
        return self.__name


class B(A):

    def test(self):
        try:
            self.__name
        except Exception as e:
            print(e)

        try:
            self.__foo()
        except Exception as e:
            print(e)


B().test()
"""
'B' object has no attribute '_B__name'
'B' object has no attribute '_B__foo'
"""

通过报错信息我们即可得知原因,B 也是一个类,那么在 B 里面调用私有属性,同样会加上 _类名 这个前缀,但是这个类名显然是 B 的类名,不是 A 的类名,因此找不到 _B__name 和 _B__foo,当然我们强制通过 _A__name 和 _A__foo 也是可以访问的,只是不建议这么做。

因此 Python 中不存在绝对的私有,只不过是解释器内部偷梁换柱将你的私有属性换了个名字罢了,但是我们可以认为它是私有的,因为按照原本的逻辑没有办法访问。同理继承的子类,也没有办法使用父类的私有属性。

但是在 Cython 中是不是这样子呢?

cdef class Person:
    cdef public:
        long __age
        str __name
        long length

    def __init__(self, name, age, length):
        self.__age = age
        self.__name = name
        self.length = length

    cdef str __get_info(self):
        return f"name: {self.__name}, age: {self.__age}, length: {self.length}"

    cdef str get_info(self):
        return f"name: {self.__name}, age: {self.__age}, length: {self.length}"

cdef class CGirl(Person):

    cpdef test1(self):
        return self.__name, self.__age, self.length

    cpdef test2(self):
        return self.__get_info()

    cpdef test3(self):
        return self.get_info()

静态类 CGirl 继承静态类 Person,那么 CGirl 对象能否使用 Person 里面的私有属性或方法呢?

import pyximport
pyximport.install(language_level=3)


import cython_test

c_g = cython_test.CGirl("古明地觉", 17, 156)
print(c_g.__name, c_g.__age, c_g.length)  # 古明地觉 17 156
print(c_g.test1())  # ('古明地觉', 17, 156)
print(c_g.test2())  # name: 古明地觉, age: 17, length: 156
print(c_g.test3())  # name: 古明地觉, age: 17, length: 156

我们看到没有任何问题,对于静态类而言,子类可以使用父类中 cdef 定义的方法。除此之外,私有属性和私有方法也是可以使用的,就仿佛这些方法定义在自身内部一样。其实根本原因就在于对于静态类而言,里面的所有属性名称、方法名称都是所见即所得,比如我们设置了 self.__name,那么它的属性名就叫做 __name,不会在属性名的前面加上 "_类名",获取的时候也是一样。所以对于静态类而言,属性(方法)名称是否以双下划线开头根本无关紧要。

然后我们再来看看 Python 类继承静态类之后会有什么表现呢?

cdef class Person:
    cdef public:
        long __age
        str __name
        long length

    def __init__(self, name, age, length):
        self.__age = age
        self.__name = name
        self.length = length

    cdef str __get_info(self):
        return f"name: {self.__name}, age: {self.__age}, length: {self.length}"

    cdef str get_info(self):
        return f"name: {self.__name}, age: {self.__age}, length: {self.length}"

class PyGirl(Person):

    def __init__(self, name, age, length, where):
        self.__where = where
        super(PyGirl, self).__init__(name, age, length)

    def test1(self):
        return self.__name, self.__age, self.length

    def test2(self):
        return self.__get_info()

    def test3(self):
        return self.get_info()

我们来测试一下:

import pyximport
pyximport.install(language_level=3)


import cython_test

py_g = cython_test.PyGirl("古明地觉", 17, 156, "东方地灵殿")
# 首先 __name、__age、length 都是在 Person 里面设置的,Person 是一个静态类
# 而我们说静态类里面没有那么多花里胡哨的,不会在以双下划线开头的成员变量前面加上 "_类名" 的
# 所以直接获取是没有问题的
print(py_g.__name)  # 古明地觉
print(py_g.__age)  # 17
print(py_g.length)  # 156

# 但是 __where 不一样,它不是在静态类中设置的,所以它是会加上 "_类名" 的
try:
    py_g.__where
except AttributeError as e:
    print(e)  # 'PyGirl' object has no attribute '__where'
print(py_g._PyGirl__where)  # 东方地灵殿

try:
    py_g.test1()
except AttributeError as e:
    print(e)  # 'PyGirl' object has no attribute '_PyGirl__name'
# 我们看到调用 test1 的时候报错了
# 原因就在于对于动态类而言,在类里面调用以双下划线开头的属性,会自动加上 "_类名",所以此时反而不正确了

try:
    py_g.test2()
except AttributeError as e:
    print(e)  # 'PyGirl' object has no attribute '_PyGirl__get_info'
# 对于调用方法也是如此,因为解释器 "自作聪明" 的加上了 "_类名",导致方法名错了
# 但此刻我们还无法判断动态类实例对象是否能够调用静态类内部使用 cdef 定义的方法,因为方法名就不对
# 所以我们再执行一下 py_g.test3() 就能真相大白了


try:
    py_g.test3()
except AttributeError as e:
    print(e)  # 'PyGirl' object has no attribute 'get_info'

因此结论很清晰了,静态类很单纯,里面的属性(方法)名称所见即所得,双下划线开头的属性(方法)对于静态类而言并没有什么特殊含义,动态类之所以不能调用是因为"多此一举"的在前面加上了 "_类名",导致方法名指定错了。然后是 cdef 定义的方法,即使是在 Cython 中,动态类也是不可以调用的,因为我们说 cdef 定义的是 C 一级的方法,它既不是 Python 的方法、也不像 cpdef 定义的时候自带 Python 包装器,因此它无法被子类继承,因此也就没有跨语言的边界。

如果将 cdef 改成 def 或者 cpdef,那么动态类就可以调用了。

在类里面使用 cpdef 定义的方法和在外部使用 cpdef 定义的函数一样,内部都不能出现闭包,换言之就是里面不能再定义函数。但是 cdef 和 def 是可以的,它们里面可以继续定义函数从而构建闭包。

但是注意:闭包对应的内层函数不可以是 cdef、cpdef 定义的函数,换言之只能是 def 定义的函数或者匿名函数。cdef、cpdef 在定义函数时只能出现在全局、或者类里面。

真正的私有

我们一直说双下划线开头的属性在静态类里面没有任何特殊的含义,因为是否私有不是通过名称前面是否有双下划线决定的,而是通过是否在类里面使用 cdef public 或者 cdef readonly 进行了声明所决定的,而且此时的私有是真正意义上的私有。如果不想让外界访问,那么外界是无论如何都访问不到的。

cdef class Person:
    # 因为 __init__ 中有 self.where = ... 这行赋值语句,其表示要给 self 加一个名为 where 的属性
    # 所以在类里面需要使用 cdef 进行声明,任何要绑定在 self 上面的属性都必须事先通过 cdef 声明好
    # 并且声明的同时还可以指定类型,比如这里是 str,表示绑定在 self 上的 where 是 str 类型
    # 那么 __init__ 中的参数 where 也必须要接收一个 str,否则 self.where = where 就是矛盾的
    cdef public:
        str where
    # 此外这里在 cdef 后面还指定了 public,如果不指定的话,那么只能是 Cython 中创建的实例对象才可以访问
    # 如果是在 Python 中导入这个类,那么实例化之后,是无法访问访问 where 属性的
    # 也就是说,如果你不希望这里的 where 属性被外界的 Python 代码所访问,那么直接通过 cdef str where 进行声明即可
    # 对于静态类而言,私有是通过这种方式实现的,而且此时的私有是真正的私有。
    # 如果不想私有、想在 Python 中被访问的话,那么就是用 cdef public 声明
    # 当然 cdef public 是外界既可以访问也可以修改,但如果只希望外界可以访问而不可以修改,那么就使用 cdef readonly
    # 上面这些之前说过了,这里再重复一下

    def __init__(self, where):
        self.where = where


cdef class CGirl(Person):

    # 这里也是同理,只不过是私有的
    cdef:
        str name
        int age
        int length

    def __init__(self, name, age, length, where):
        self.name = name
        self.age = age
        self.length = length
        super(CGirl, self).__init__(where)

但是注意:对于 CGirl 而言,我们不需要声明 where,因为 self.where 的绑定是在 Person 中发生的,只要在 Person 中声明即可。由于 CGirl 继承 Person,如果 CGirl 中也声明了 where 那么返而报错,提示 where 重复声明了。

import pyximport
pyximport.install(language_level=3)

import cython_test


c_g = cython_test.CGirl("古明地觉", 16, 157, "东方地灵殿")
# where 是使用 cdef public 声明的,所以不是私有的
# name、age、length 是使用 cdef 声明的,所以是私有的
print(c_g.where)  # 东方地灵殿
print(hasattr(c_g, "where"))  # True
print(hasattr(c_g, "name"))  # False
print(hasattr(c_g, "age"))  # False
print(hasattr(c_g, "length"))  # False

创建不可被继承的类

但是问题来了,如果我们希望自定义的扩展类不可以被其它类继承的话该怎么做呢?

cimport cython

# 通过 cython.final 进行装饰,那么这个类就不可被继承了
@cython.final
cdef class NotInheritable:
    pass

通过 cython.final,那么被装饰的类就是一个不可继承类,不光是外界普通的 Python 类,内部的扩展类也是不可继承的。

import pyximport
pyximport.install(language_level=3)

import cython_test

class A(cython_test.NotInheritable):
    pass
"""
TypeError: type 'cython_test.NotInheritable' is not an acceptable base type
"""

告诉我们 NotInheritable 不是一个合法的基类。

让扩展类实例可以被弱引用

我们知道 Python 的每一个对象都会有一个引用计数,当一个变量引用它时,引用计数会增加一。但我们可以对一个对象进行弱引用,弱引用的特点就是不会使对象的引用计数增加,举个栗子:

import sys
import weakref


class Girl:

    def __init__(self):
        self.name = "古明地觉"


g = Girl()
# 因为 g 作为 sys.getrefcount 的参数,所以引用计数会多 1
print(sys.getrefcount(g))  # 2

g2 = g
# 又有一个变量引用,所以引用计数增加 1,结果是 3
print(sys.getrefcount(g))  # 3

# 注意:这里是一个弱引用,不会增加引用计数
g3 = weakref.ref(g)
print(sys.getrefcount(g))  # 3
print(g3)  # <weakref at 0x000001BFE84EF400; to 'Girl' at 0x000001BFCF9BA5B0>

# 删除 g、g2,对象的引用计数会变为 0,此时再打印 g3,会发现引用的对象已经被销毁了
del g, g2
print(g3)  # <weakref at 0x00000222F0ED13B0; dead>

默认情况下,动态类实例对象都是可以被弱引用的,我们可以查看类的属性字典:

print(Girl.__dict__["__weakref__"])  # <attribute '__weakref__' of 'Girl' objects>

但是也有一个特例:

import weakref

class Girl:

    __slots__ = ("name",)
    def __init__(self):
        self.name = "古明地觉"

# 一旦当定义了 __slots__ 的时候,这个类的实例对象就不可以被引用了
try:
    weakref.ref(Girl())
except TypeError as e:
    print(e)  # cannot create weak reference to 'Girl' object

# 如果希望在定义 __slots__ 的时候也希望能被引用,那么就把 "__weakref__" 也加进去
class Girl:

    __slots__ = ("name", "__weakref__")
    def __init__(self):
        self.name = "古明地觉"

# 此时就可以被弱引用了,只不过此时引用的对象也已经被销毁了,因为我们没有用变量指向它
print(weakref.ref(Girl()))  # <weakref at 0x000002244C8CF450; dead>

那么问题来了,静态类或者说扩展类的实例对象可不可以被弱引用呢?我们拿内置类型来试试吧,因为我们说在 Cython 中定义的扩展类和内置的类是等价的,它们同属于静态类,如果内置类型的实例对象不可以被弱引用的话,那么 Cython 中定义的扩展类也是一样的结果。

import weakref


try:
    weakref.ref(123)
except TypeError as e:
    print(e)  # cannot create weak reference to 'int' object

try:
    weakref.ref("")
except TypeError as e:
    print(e)  # cannot create weak reference to 'str' object

try:
    weakref.ref(())
except TypeError as e:
    print(e)  # cannot create weak reference to 'tuple' object

try:
    weakref.ref({})
except TypeError as e:
    print(e)  # cannot create weak reference to 'dict' object

我们看到内置类型的实例是不可以被弱引用的,那么扩展类必然也是如此,其实也很好理解,因为要保证速度,自然会丧失一些 "花里胡哨" 的功能。但是问题来了,扩展类是我们自己实现的,我们就让其实例可以被弱引用该怎么办呢?

cdef class A:
    # 类似于动态类中的 __slots__,只需要声明一个 __weakref__ 即可
    cdef object __weakref__

cdef class B:
    pass
import weakref
import pyximport

pyximport.install(language_level=3)
import cython_test

# A 实例是可以被引用的,因为我们指定了 __weakref__
print(weakref.ref(cython_test.A()))  # <weakref at 0x0000016E962D2220; dead>

# 但是 B 实例默认则是不允许的,因为它是没有指定 __weakref__ 的静态类
try:
    print(weakref.ref(cython_test.B()))
except TypeError as e:
    print(e)

以上就是让扩展类实例支持弱引用的方式。

扩展类实例对象的销毁以及垃圾回收

我们说当对象的引用计数为 0 时,会被销毁,这个销毁可以是放入缓存池中、也可以是交还给系统堆,当然不管哪一种,我们都不能再用了。

x = "古明地觉"
x = "古明地恋"

在执行完第二行的时候,由于 x 指向了别的字符串,因此第一个字符串对象引用计数为 0、会被销毁。而这个过程在底层会调用 tp_dealloc,因为 Python 的类对象在底层对应的都是一个 PyTypeObject 结构体实例,其内部有一个 tp_dealloc 成员是专门负责其实例对象的销毁的。

因此判断一个 Python 中的对象是否会被销毁非常简单,就看它的引用计数,只要引用计数为 0,就会被销毁,不为 0,就不会被销毁,就这么简单。但是引用计数最大的硬伤就是它解决不了循环引用,所以 Python 才会有垃圾回收机制,专门负责解决循环引用。

class Object:
    pass


def make_cycle_ref():
    x = Object()
    y = [x]
    x.attr = y 

当我们调用 make_cycle_ref 函数时,就会出现循环引用,x 内部引用了 y 指向的对象、y 内部又引用了 x 指向的对象,如果我们将垃圾回收机制关闭的话,即使函数退出,对象也不会被回收。而如果想解决这一点,那么就必须在销毁对象之前先将对象内部引用的其它对象的引用计数减一才行,也就是打破循环引用,这便是 Python 底层的垃圾回收器所做的事情。

所以上面在销毁 x 时(x 是一个变量,这里为了描述方便,就用 x 代指其指向的对象,后面的 y 同理),先将内部的属性 attr 引用的 y 的引用计数减 1,然后再将 x 的引用计数减 1,但是此时 x 还没有销毁,因为它的引用计数还是 1;然后销毁 y,在销毁 y 之前,先将内部引用的 x 的引用计数减 1,此时 x 的引用计数就变成 0 了。接着再将 y 的引用计数减 1,由于在销毁 x 的时候,已经将 y 的引用计数减 1 了,所以此时 y 的引用计数也为 0。所以最终 x 和 y 的引用计数都会变成 0,最终都会被销毁,所以垃圾回收只是将发生循环引用的对象的引用计数减去 1,而对象是否被销毁还是由引用计数是否为 0 所决定的。

而如果想做到这一点,那么就必须在 tp_traverse 中指定垃圾回收器要跟踪的属性,PyTypeObject 内部有一个 tp_traverse 成员,它接收一个函数,在内部指定要跟踪的属性(x 的话就是 attr,y 由于是一个列表,它里面的每一个元素都要跟踪)。垃圾回收器根据 tp_traverse 指定的要跟踪的属性,找到这些属性引用的其它对象;然后 PyTypeObject 内部还有一个 tp_clear,在这里面再将引用的其它对象的引用计数减 1,所以寻找(tp_traverse)和清除(tp_clear)是在两个函数中实现的。

  • tp_traverse:指定垃圾回收器要跟踪的属性,垃圾回收器会找到这些属性引用的对象
  • tp_clear:将 tp_traverse 中找到的属性引用的对象的引用计数减 1
  • tp_dealloc:负责对象本身被销毁时的工作,在扩展类中可以用 __dealloc__ 实现

禁用 tp_clear

对于扩展类而言,默认是支持垃圾回收的,底层会自动生成 tp_traverse 和 tp_clear,显然这也是我们期待的结果。但在某些场景下,就不一定是我们期待的了,比如你需要在 __dealloc__ 中清理某些外部资源,但是你的对象又恰好在循环引用当中,举个栗子:

cdef class DBCursor:
    cdef DBConnection conn
    cdef DBAPI_Cursor *raw_cursor
    # ...
    def __dealloc__(self):
        DBAPI_close_cursor(self.conn.raw_conn, self.raw_cursor)

当我们在销毁对象时,想要通过数据库连接来关闭游标,但如果游标碰巧处于循环引用当中,那么垃圾回收器可能会删除数据库连接,从而无法对游标进行清理。所以解决办法就是禁用该扩展类的 tp_clear,通过 no_gc_clear 装饰器来实现:

cimport cython

@cython.no_gc_clear
cdef class DBCursor:
    cdef DBConnection conn
    cdef DBAPI_Cursor *raw_cursor
    # ...
    def __dealloc__(self):
        DBAPI_close_cursor(self.conn.raw_conn, self.raw_cursor)

如果使用 no_gc_clear,那么引用循环中至少有一个没有 no_gc_clear 的对象,否则循环引用无法被打破,从而引发内存泄露。

禁用垃圾回收

我们说垃圾回收是为了解决循环引用而存在的,解释器会将那些能产生循环引用的对象放在链表(零代、一代、二代)上,然后从根节点出发进行遍历,而显然链表上的对象越少,垃圾回收的耗时就越短。所以如果一个对象可以发生循环引用,但是我们能保证实际情况中它不会发生,那么我们就可以让这个对象不参与垃圾回收,从而提升效率。

比如我们可以定义一个不可能发生循环引用的扩展类:

cdef class Girl:
    
    cdef public str name
    cdef public int age

我们知道扩展类的实例对象是可以发生循环引用的,所以它默认会被挂到链表上,但是很明显,对于我们当前这个扩展类而言,它的实例对象是不会发生循环引用的。因为内部只有两个属性,分别是字符串和整型,加上扩展类无法动态添加属性,所以实际情况中 Girl 的实例不可能产生循环引用。但是解释器不会做这种假设,所以依旧会将其挂到链表上,因此我们可以使用 no_gc 装饰器来阻止解释器这么做。

cimport cython

@cython.no_gc
cdef class Girl:
    
    cdef public str name
    cdef public int age

此时程序的运行效率会得到提升,当然要注意:使用 no_gc 一定要确保不会发生循环引用,如果给上面的类再添加一个声明:

cimport cython

@cython.no_gc
cdef class Girl:
    
    cdef public str name
    cdef public int age
    cdef public list hobby

这个时候就必须要小心了,因为里面出现了列表,列表是可以发生循环引用的。

启动 trashcan

在 Python 中,我们可以创建具有深度递归的对象,比如:

L = None

for i in range(2 ** 20):
    L = [L]
    
del L

当我们删除 L 的时候,会先销毁打印 L[0]、然后销毁 L[0][0],以此类推,直到递归深度为 2 ** 20。而这样的深度很容易溢出 C 的调用栈,导致 Python 解释器崩溃,但事实上我们上面在 del L 的时候解释器并没有崩溃,原因就是 CPython 发明了一种名为 trashcan 的机制,它通过延迟销毁的方式来限制销毁的递归深度。比如可以查看 CPython 源代码 Object/listobject.c 的 list_dealloc 函数,在销毁列表时有体现:

static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_BEGIN(op, list_dealloc)  // 限制销毁的递归深度
    /*
    ...
    ...
    ...
    */
    Py_TRASHCAN_END
}

但是对于扩展类而言,则并非如此,它默认是没有开启 transcan 机制的:

cdef class A:

    def __init__(self):
        cdef list L = None
        cdef unsigned long i
        for i from 0 <= i < 2 ** 30:
            L = [L]

如果你导入 A 这个类并实例化,那么你的内存占用率会越来越高,最终程序崩溃。如果希望扩展类实例对象也能开启 transcan 机制,同样可以使用装饰器:

cimport cython

@cython.transcan(True)
cdef class A:

    def __init__(self):
        cdef list L = None
        cdef unsigned long i
        for i from 0 <= i < 2 ** 30:
            L = [L]

如果一个类开启了 transcan 机制,那么继承它的子类也会开启,如果不想开启,则需要通过 transcan(False) 显式关闭。

说实话以上这些其实不是特别常用,一旦使用就意味着你要格外小心,因为涉及内存都是有危险的,所以如果无法百分百确认自己是否需要这么做,那就不要做。而且像 transcan 这种,个人觉得真正写项目的时候很少会出现,了解一下即可。

扩展类实例的 freelist

有些时候我们需要多次对某个类执行实例化和销毁操作,这也意味着会有多次内存的创建与销毁。那么我们能不能像Python 底层采用的缓存池策略一样,每次销毁的时候不释放内存,而是放入一个链表(freelist)中,当申请的时候直接从链表中获取即可。

cimport cython

# 声明一个可以容纳 8 个实例的链表,每当销毁的时候就会放入到链表中,最多可以放 8 个
# 如果销毁第 9 个实例,那么就不会再放到 freelist 里了
@cython.freelist(8)
cdef class Girl:

    cdef str name
    cdef int age

    def __init__(self, name, age):
        self.name = name
        self.age = age 


girl = Girl("古明地觉", 16)
del girl  # 会放入到 freelist 中,里面的元素个数加 1
girl = Girl("雾雨魔理沙", 17)  # 从 freelist 中获取,里面元素个数减 1,此时无需重新申请内存

扩展类实例的序列化和反序列化

然后说一下序列化和反序列化,像内置 pickle、json 库都可以将对象序列化和反序列化,这里我们说的是 pickle。pickle 和 json 不同,json 序列化之后是人类可阅读的,但是能序列化的对象是有限的,因为序列化的结果可以在不同语言之间传递;但是 pickle 序列化之后是二进制格式,只有 Python 才认识,因此它可以序列化 Python 的绝大部分对象。

import pickle


class Girl:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age


girl = Girl("古明地觉", 16)
# 这便是序列化话的结果
dumps_obj = pickle.dumps(girl)
# 显然这是什么东西我们不认识,但是 Python 解释器认识,我们可以再进行反序列化
print(dumps_obj[: 20])  # b'\x80\x04\x95;\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main_'

loads_obj = pickle.loads(dumps_obj)
print(loads_obj.name)  # 古明地觉
print(loads_obj.age)  # 16

这里我们不探究 pickle 的实现原理,我们来说一下如何自定制序列化和返序列化的过程,想要自定制的话,需要实现 __getstate__ 和 __setstate__ 两个魔法方法:

import pickle


class Girl:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def __getstate__(self):
        """序列化的时候会调用"""
        # 对 Girl 的实例对象进行序列化的时候,默认会返回其属性字典
        # 这里我们多添加一个属性
        print("被序列化了")
        return {**self.__dict__, "gender": "女"}

    def __setstate__(self, state):
        """反序列化时会调用"""
        # 对 Girl 的实例对象进行序列化的时候,会将 __getstate__ 返回的字典传递给这里的 state 参数
        # 我们再设置到 self 当中,如果不设置,那么序列化之后是无法获取属性的
        print("被反序列化了")
        self.__dict__.update(**state)


girl = Girl("古明地觉", 16)
dumps_obj = pickle.dumps(girl)
"""
被序列化了
"""

loads_obj = pickle.loads(dumps_obj)
print(loads_obj.name)  
print(loads_obj.age)  
print(loads_obj.gender) 
"""
被反序列化了
古明地觉
16
女
"""

																				
															

推荐阅读