打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C 一级的构造函数和析构函数

每一个实例对象都对应了一个 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(3344)
"""
__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。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
移动开发:iOS内存管理之:引用计数、ARC、自动释放池autoreleasepool和便捷方法之间的关系
Cython应用手记
史上最细致讲解!Python 5大常用魔术方法,一学就会
关于内存泄漏,还有哪些是你不知道的?
view和viewController的生命周期
Objective
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服