我们之前介绍了定义函数的三个关键字,分别是 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 带来的性能改进,后续我们将了解关于继承、子类化、和扩展类型的多态性相关的知识。