每一个实例对象都对应了一个 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。
联系客服