每一个实例对象都对应了一个 C 结构体,其指针就是类型对象里面的 self,我们以 __init__ 为例。当 __init__ 被调用时,会对 self 进行属性的初始化,而且 __init__ 是自动调用的。
但是我们知道在 __init__ 调用之前,会先调用 __new__, __new__ 的作用就是为创建的实例对象开辟一份内存,然后返回其指针并交给 self。在 C 层面就是,调用 __init__ 之前,实例对象对应的结构体必须已经分配好内存,并且结构体的所有字段都处于可以接收初始值的有效状态。
Cython 扩充了一个名为 __cinit__ 的特殊方法,用于执行 C 级别的内存分配和初始化。如果不涉及 C 级别的内存分配,那么只需要使用 __init__。但如果涉及 C 级别的内存分配,那么就不可以使用 __init__ 了,而是需要使用 __cinit__。
""" 我们说过 Cython 同时理解 C 和 Python 所以 C 的一些标准库在 Cython 里面也可以用 比如 stdio.h、stdlib.h 等等 在 Cython 里面直接通过 libc 导入即可 比如 from libc cimport stdlib, stdio 然后通过 stdlib.malloc、stdlib.free 调用 """ # 当然也可以导入具体的函数 from libc.stdlib cimport malloc, free cdef class A: cdef: Py_ssize_t n # 一个指针,指向了 double 类型的数组 double *array 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 """ # 在 __dealloc__ 里面用于释放堆内存 if self.array != NULL: free(self.array) def set_value(self): cdef Py_ssize_t i for i in range(self.n): self.array[i] = (i + 1) * 2 def get_value(self): cdef Py_ssize_t i for i in range(self.n): print(self.array[i])
编译测试,文件名叫 cython_test.pyx:
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__ 和 __dealloc__ 是成对出现的,即使在 __cinit__ 里面没有 C 一级的内存分配,也必须要定义 __dealloc__。但如果不涉及 C 一级的内存分配,我们也没必要定义 __cinit__。
这个时候可能有人好奇了,__cinit__ 和 __init__ 函数有什么区别呢?区别还是蛮多的,我们细细道来。
首先它们只能通过 def 来定义,另外在不涉及 malloc 动态分配内存的时候, __cinit__ 和 __init__ 是等价的。然而一旦涉及到 malloc,那么动态分配内存只能在 __cinit__ 中进行。如果这个过程写在了 __init__ 中,比如将我们上面例子的 __cinit__ 改为 __init__ 的话,你会发现 self 的所有变量都没有设置进去、或者说设置失败,并且其它的方法若是访问了 self.array,还会导致丑陋的段错误。
还有一点就是,__cinit__ 会在 __init__ 之前调用,我们实例化一个扩展类的时候,参数会先传递给 __cinit__,然后 __cinit__ 再将接收到的参数原封不动的传递给 __init__。
cdef class A: cdef public: Py_ssize_t a, b def __cinit__(self, a, b): print("__cinit__") print(a, b) def __init__(self, c, d): """ __cinit__ 中接收两个参数 然后会将参数原封不动的传递到这里 所以这里也要接收两个参数 参数名可以不一致,但是个数和类型要匹配 """ print("__init__") print(c, d) A(33, 44) """ __cinit__ 33 44 __init__ 33 44 """
所以当涉及 C 级别的内存分配时使用 __cinit__,如果没有涉及那么使用 __init__ 就可以,虽然在不涉及 malloc 的时候这两者是等价的,但是 __cinit__ 会比 __init__ 的开销要大一些。而如果涉及 C 级别内存分配,那么建议 __cinit__ 只负责内存的动态分配,__init__ 负责其它属性的创建。
from libc.stdlib cimport malloc, free cdef class A: cdef public: Py_ssize_t 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 = <double *>malloc(3 * sizeof(double)) 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, Py_ssize_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, Py_ssize_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。