扩展类的成员函数

简介: 扩展类的成员函数

我们之前介绍了定义函数的三个关键字,分别是 def, cdef 和 cpdef。def 定义的就是纯 Python 函数,可以被外界访问,但是性能不足;cdef 定义的是高性能的 C 函数,但是无法被外界访问。

而 cpdef 定义的函数既可以在 Cython 内部访问,也可以被外界访问,因为它相当于定义了两个版本的函数:一个是高性能的纯 C 版本,另一个是 Python 包装器(相当于我们手动定义的 Python 函数)。所以这就要求使用 cpdef 定义的函数的参数和返回值类型必须是 Python 可以表示的,像 char * 之外的指针就不行。

同理,def, cdef 和 cpdef 也可以定义扩展类的成员函数,其表现和定义普通函数是一样的。但是注意:cdef 和 cpdef 修饰的成员函数必须位于 cdef class 定义的扩展类里面,如果是 class 定义的动态类,那么成员函数只能用 def 定义。

cdef class A:
    cdef public:
        Py_ssize_t a, b
    def __init__(self, a, b):
        self.a = a
        self.b = b
    
    # cdef 修饰
    cdef Py_ssize_t f1(self):
        return self.a * self.b
    
    # cpdef 修饰
    cpdef Py_ssize_t f2(self):
        return self.a * self.b

文件名为 cython_test.pyx,我们编译测试一下:

import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A(3, 5)
print(a.f2()) 
"""
15
"""
try:
    a.f1()
except AttributeError as e:
    print(e)
"""
'cython_test.A' object has no attribute 'f1'
"""

f1 是 cdef 定义的,所以它无法被外界访问,cdef 和 cpdef 之间在函数上的差异,在方法中得到了同样的体现。

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

cdef class A:
    cdef public:
        Py_ssize_t a, b
    def __init__(self, a, b):
        self.a = a
        self.b = b
    cpdef Py_ssize_t f2(self):
        return self.a * self.b
    
def traverse(ins_lst):
    s = 0
    for ins in ins_lst:
        s += ins.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.traverse([a1, a2, a3])
)  # 16

这就是是 Python 的特性,一切都是对象,尽管没有指明 ins_lst 指向什么类型,但只要它可以被 for 循环即可。尽管没有指明 ins_lst 里面的元素指向什么类型,只要它有 f2 方法即可。

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

# 规定接收一个 list,返回一个 Py_ssize_t
# 它们都是静态的,总之静态类型越多,速度会越快
cpdef Py_ssize_t traverse(list ins_lst):
    # 声明 Py_ssize_t 类型的 s,A 类型的 ins
    cdef Py_ssize_t s = 0
    cdef A ins
    for ins in ins_lst:
        s += ins.f2()
    return s

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

这个版本的速度比之前快了 10 倍,这表明类型化比非类型化要快了 10 倍。如果我们删除了 cdef A self,也就是不规定类型,而还是按照 Python 的语义来调用,那么速度仍然和之前一样,即便使用 cpdef 定义。

所以重点在于指定类型为静态类型,只要规定好类型,让变量具体指向 C 一级的数据结构,那么就可以提升速度。如果是 int 和 float,那么 Cython 会自动采用 C 的 int 和 float,这样速度能进一步提升,当然怕溢出的话就使用 size_t, Py_ssize_t, double 等类型

因此重点是一定要静态定义类型,只要类型明确,那么就能进行大量的优化。

前面说过 Python 慢主要有两个原因,一个是它无法对类型进行优化,另一个是对象分配在堆上。

1)无法基于类型进行优化,就意味着每次都要进行大量的检测,如果规定好类型,那么就不用兜那么大圈子了;

2)对象分配在堆上这是无法避免的,只要你用 Python 的对象,都是分配在堆上。当然你可以用 C++ 中的结构进行替换,但这会增加代码的复杂度,而且也不像是写 Python 了。但是对于整型和浮点型,我们可以通过 cdef 将其声明为 C 的类型,使对象分配在栈上,进一步提升效率。

总之记住一句话:Cython 加速的关键就在于,类型的静态声明,以及将整型和浮点型换成 C 的类型。

成员函数中的参数类型

无论是 def、cdef、cpdef 定义的是普通函数,还是类的成员函数,它们的表现都是一致的,都是函数。所以在定义的时候,都可以给参数规定类型,如果类型传递的不对就会报错。

比如上面的 traverse 函数,如果不规定参数类型,那么参数只要能够被 for 循环即可,所以它可以是列表、元组、集合。但是我们上面规定了是 list 类型,那么参数只能传递 list 对象或者其子类的实例对象,如果传递 tuple 对象就会报错。

这里我们来看一下 __init__

cdef class A:
    cdef public:
        Py_ssize_t a, b
    def __init__(self, float a, float b):
        self.a = a
        self.b = b

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

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

以上就是类的成员函数,和普通函数的表现是一致的。但是扩展类的内容还没有结束,为了更好地解释 Cython 带来的性能改进,后续我们将了解关于继承、子类化、和扩展类型的多态性相关的知识。

相关文章
|
7月前
|
编译器 C++
【C++11特性篇】新的类功能解读:新增加的[移动构造函数/移动赋值运算符重载]
【C++11特性篇】新的类功能解读:新增加的[移动构造函数/移动赋值运算符重载]
|
1月前
|
存储 JavaScript 前端开发
构造函数继承有什么缺点?
【10月更文挑战第26天】构造函数继承虽然能够实现属性的继承,但在方法继承、内存使用效率、访问父类原型属性以及实现多态性等方面存在一些缺点。在实际开发中,可以根据具体的需求和场景,结合其他继承方式来综合解决这些问题,以实现更高效、更灵活的继承机制。
35 8
|
2月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
38 3
|
2月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
46 1
|
6月前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
7月前
|
安全 编译器 程序员
类与对象(二)--类的六个默认成员函数超详细讲解
类与对象(二)--类的六个默认成员函数超详细讲解
类与对象(二)--类的六个默认成员函数超详细讲解
|
7月前
|
存储 编译器 程序员
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
|
安全 编译器 C++
[C++] 类与对象(中)类中六个默认成员函数(1)上
[C++] 类与对象(中)类中六个默认成员函数(1)上
|
7月前
|
存储 C++
c++类和对象一对象特性一成员变量和成员函数分开存储
c++类和对象一对象特性一成员变量和成员函数分开存储
49 0
|
C++
47 C++ - 继承中的静态成员特性
47 C++ - 继承中的静态成员特性
45 0