1、Python类和扩展类之间的差异

首先 Python 中 “一切皆对象”,怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,通过 id 函数可以获取地址并将每一个对象都区分开来,使用 type 获取类型。Python 中对象有很多属性,这些属性都放在自身的属性字典里面,这个字典可以通过 __dict__ 获取。调用对象的某一个属性的时候,可以通过 . 的方式来调用,Python 也允许我们通过 class 关键字自定义一个类。

1
2
3
4
5
6
7
8
9
10
11
12
class A:
pass

print(A.__name__) # A
A.__name__ = "B"
print(A.__name__) # B

try:
int.__name__ = "INT"
except Exception as e:
# 内建类型 和 扩展类型 不允许修改属性
print(e) # can't set attributes of built-in/extension type 'int'

除了在 Python 中定义类,还可以直接使用 Python/C API 在 C 级别创建自己的类型,这样的类型称之为扩展类、或者扩展类型(说白了在 C 中实现的类就叫做扩展类)。

Python 解释器本来就是 C 写的,所以可以在 C 的层面上面实现 Python 的任何对象,类也是如此。Python 中自定义的类和内置的类在 C 一级的结构是一致的,所以只需要按照 Python/C API 提供的标准来编写即可。但还是那句话,使用 C 来编写会比较麻烦,因为本质上就是写 C 语言。

当操作扩展类的时候,操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的 C 一级的访问,这种访问可以显著的提高性能。但是在扩展类的实现、以及处理相应的实例对象和在纯 Python 中定义类是完全不同的,需要有专业的 Python/C API 的知识,不适合新手。

这也是 Cython 要增强 Python 类的原因:Cython 使得创建和操作扩展类就像操作 Python 中的类一样。在Cython中定义一个扩展类通过 cdef class 的形式,和 Python 中的常规类保持了高度的相似性。

尽管在语法上有着相似之处,但是 cdef class 定义的类对所有方法和数据都有快速的 C 级别的访问,这也是和扩展类和 Python 中的普通类之间的一个最显著的区别。而且扩展类和 int、str、list 等内置的类都属于静态类,它们的属性是不可修改的。

2、Cython中的扩展类

写一个 Python 中的类

1
2
3
4
5
6
7
8
class Rectangle:

def __init__(self, width, height):
self.width = width
self.height = height

def get_area(self):
return self.width * self.height

这个类是在 Python 级别定义的,可以被 CPython 编译的。定义了矩形的宽和高,并且提供了一个方法,计算面积。这个类是可以动态修改的,可以指定任意的属性。

如果对这个 Python 类编译的话,那么得到的类依旧是一个纯 Python 类,而不是扩展类。所有的操作,仍然是通过动态调度通用的 Python 对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的 Cython 代码仍然需要在运行时动态调度来解析类型。

改成扩展类的话,需要这么做。

1
2
3
4
5
6
7
8
9
10
11
# cython_test.pyx
cdef class Rectangle:

cdef long width, height

def __init__(self, w, h):
self.width = w
self.height = h

def get_area(self):
return self.width * self.height

此时的关键字使用的是cdef class,意思就是表示这个类不是一个普通的 Python 类,而是一个扩展类。内部代码,多了一个 cdef long width, height,这个是名称和 self 的属性是同名的,表示 self 中的 width、height 都必须是一个 long,或者说可以转为 C 中的 long 的 Python 对象。另外对于 cdef 来说,定义的类是可以被外部访问的,虽然函数不行、但类可以

1
2
3
4
5
6
7
8
9
10
11
import pyximport
pyximport.install(language_level=3)

import cython_test
rect = cython_test.Rectangle(3, 4)
print(rect.get_area()) # 12

try:
rect = cython_test.Rectangle("3", "4")
except TypeError as e:
print(e) # an integer is required

注意:在 __init__ 中实例化的属性,都必须在类中使用 cdef 声明,举个栗子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cdef class Rectangle:
# 这里我们只声明了width, 没有声明height, 那么是不是意味着这个height可以接收任意对象呢?
cdef long width

def __init__(self, w, h):
self.width = w
self.height = h

def get_area(self):
return self.width * self.height
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
"""
File "cython_test.pyx", line 7, in cython_test.Rectangle.__init__
self.height = h
AttributeError: 'cython_test.Rectangle' object has no attribute 'height'
"""

凡是在没有在 cdef 中声明的,都不可以赋值给 self,可能有人发现了这不是访问,而是添加呀。添加一个属性咋啦,没咋,无论是获取还是赋值,self 中的属性必须使用 cdef 在类中声明。举一个Python 内置类型的例子吧:

1
2
3
4
5
a = 1
try:
a.xx = 123
except Exception as e:
print(e) # 'int' object has no attribute 'xx'

一样等价,扩展类和内建的类是同级别的,一个属性如果想通过 self. 的方式来调用,那么一定要在类里面通过 cdef 声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
cdef class Rectangle:
cdef long width, height

def __init__(self, w, h):
self.width = w
self.height = h

def get_area(self):
return self.width * self.height


import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)

try:
rect.a = "xx"
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute 'a'
"""
如果想动态修改、添加类型,那么需要解释器在解释的时候来动态操作
但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力
也正因为如此,也能提高效率。因为很多时候,我们不需要动态修改。
当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里
其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr 等价于instance.__dict__["attr"],同理修改、创建也是。
但是注意:这只是针对普通的 Python 类而言,但扩展类的实例对象内部是没有 __dict__ 的。
"""

try:
rect.__dict__
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute '__dict__'

# 不光 __dict__, 你连 self 本身的属性都无法访问
try:
rect.width
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute 'width'
# 提示我们 self 没有 width 属性,所以我们实例化之后再想修改是不行的,连获取都获取不到
# 只能调用它的一些方法罢了。

所以内建的类和扩展类是完全类似的,其实例对象都没有属性字典,至于类本身是有属性字典的,但是这个字典不可修改。因为虽然叫属性字典,但它的类型实际上是一个 mappingproxy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pyximport
pyximport.install(language_level=3)

import cython_test

try:
int.__dict__["a"] = 123
except TypeError as e:
print(e) # 'mappingproxy' object does not support item assignment

try:
cython_test.Rectangle.__dict__["a"] = 123
except TypeError as e:
print(e) # 'mappingproxy' object does not support item assignment

还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候是允许你做一些这样的骚操作的。但是内置的类和扩展类是不需要解释这一步的,它们是彪悍的人生,直接指向了 C 一级的数据结构,因此也就丧失了这种动态的能力。

但是扩展类毕竟是自己指定的,如果想修改 self 的一些属性呢?答案是将其暴露给外界即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cdef class Rectangle:
# 通过cdef public的方式进行声明即可
# 这样的话就会暴露给外界了
cdef public long width, height

def __init__(self, w, h):
self.width = w
self.height = h

def get_area(self):
return self.width * self.height


import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
print(rect.width) # 3
print(rect.get_area()) # 12

rect.width = 123
print(rect.get_area()) # 492

try:
rect.__dict__
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute '__dict__'
# 属性字典依旧是没有的

通过 cdef public声明的属性,是可以被外界获取并修改的,除了cdef public之外还有 cdef readonly,同样会将属性暴露给外界,但是只能访问不能修改。

1
2
3
4
5
6
7
8
9
10
11
12
import pyximport
pyximport.install(language_level=3)

import cython_test

rect = cython_test.Rectangle(3, 4)
print(rect.width) # 3

try:
rect.width = 123
except AttributeError as e:
print(e) # attribute 'width' of 'cython_test.Rectangle' objects is not writable
  • cdef readonly 类型 变量名:实例属性可以被访问,但是不可以被修改
  • cdef public 类型 变量名:实例属性可以被访问,也可以被修改
  • cdef 类型 变量名:实例属性既不可以被访问,更不可以被修改

当然定义变量无论是使用 cdef public还是 cdef readonly,Cython 内部的方法都可以实行快速访问,因为扩展类的方法基本上忽略了 readonly 和 public 的声明,它们存在的目的只是为了控制来自外界的访问。

3、C一级的构造函数和析构函数

每一个实例对象都对应了一个 C 结构体,其指针就是 Python 调用__init__函数里面的 self 参数。当__init__参数被调用时,会初始化 self 参数上的属性,而且__init__参数是自动调用的。但是在 __init__参数调用之前,会先调用__new__方法, __new__方法的作用就是为创建的实例对象开辟一份内存,然后返回其指针并交给 self。在 C 级别就是,在调用__init__之前,实例对象指向的结构体必须已经分配好内存,并且所有结构字段都处于可以接收初始值的有效状态。

Cython 扩充了一个名为__cinit__的特殊方法,用于执行 C 级别的内存分配和初始化。不过对于之前定义的 Rectangle 类的 __init__方法,因为内部的字段接收的值是两个 double,不需要 C 级别的内存分配。但如果需要 C 级别的内存分配,那么就不可以使用 __init__了,而是需要使用 __cinit__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 导入相关函数,malloc,free
# 如果不熟悉的话,建议去了解一下C语言
from libc.stdlib cimport malloc, free

cdef class A:
cdef:
unsigned int n
double *array # 一个数组,存储了double类型的变量

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
然后我们释放掉指针指向的内存
"""
if self.array != NULL:
free(self.array)

def set_value(self):
cdef long i
for i in range(self.n):
self.array[i] = (i + 1) * 2

def get_value(self):
cdef long i
for i in range(self.n):
print(self.array[i])


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____init__ 函数有什么区别呢?

首先它们只能通过 def 来定义,另外在不涉及 malloc 动态分配内存的时候, __cinit____init__是等价的。然而一旦涉及到 malloc,那么动态分配内存只能在 __cinit__中进行,如果这个过程写在了__init__函数中,比如将上面例子的__cinit__改为 __init__的话,你会发现 self 的所有变量都没有设置进去、或者说设置失败,并且其它的方法若是引用了 self.array,那么还会导致丑陋的段错误。

还有一点就是,__cinit__ 函数会在 __init__函数之前调用,实例化一个扩展类的时候,参数会先传递给 __cinit__,然后 __cinit__再将接收到的参数原封不动的传递给__init__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cdef class A:
cdef public:
unsigned int a, b

def __cinit__(self, a, b):
print("__cinit__")
self.a = a
self.b = b
print(self.a, self.b)

def __init__(self, c, d):
"""__cinit__ 中接收两个参数
然后会将参数原封不动的传递到这里,所以这里也要接收两个参数
参数名可以不一致,但是个数要匹配
"""
print("__init__")
print(c, d)


import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(111, 222)
"""
__cinit__
111 222
__init__
111 222
"""
print(a.a) # 111
print(a.b) # 222

注意:__cinit__ 只有在涉及 C 级别内存分配的时候才会出现,如果没有涉及那么使用 __init__ 就可以,虽然在不涉及 malloc 的时候这两者是等价的,但是 __cinit__会比 __init__ 的开销要大一些。而如果涉及 C 级别内存分配,那么建议 __cinit__ 只负责内存的动态分配,__init__负责属性的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from libc.stdlib cimport malloc, free

cdef class A:

cdef public:
unsigned int 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 = <int *>malloc(3 * sizeof(int))

def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c

def __dealloc__(self):
free(self.array)

4、cdef和cpdef方法

之前使用了 cdef 和 cpdef,cdef 可以定义变量和函数,但是不能被 Python 直接访问;可以定义一个类,能直接被外界访问。而 cpdef 专门用于定义函数,cpdef 定义的函数既可以在 Cython 内部访问,也可以被外界访问,因为它定义了两个版本的函数:一个是高性能的纯C版本(此时等价于 cdef,至于为什么高效,因为它是 C 一级的,直接指向了具体数据结构,当然还有其它原因,),另一个是 Python 包装器(相当于手动定义的 Python 函数),所以还要求使用cpdef定义的函数的参数和返回值类型必须是 Python 可以表示的,像 char * 之外的指针就不行。

那么同理它们也可以作用于方法,当然方法也是实例对象在获取函数的时候进行封装得到的,所以一样的道理。但是注意:cdef 和 cpdef 修饰的 cdef class 定义的静态类里面的方法,如果是 class 定义的纯 Python 类,那么内部是不可以出现 cdef 或者 cpdef 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cdef class A:

cdef public:
long a, b

def __init__(self, a, b):
self.a = a
self.b = b

cdef long f1(self):
return self.a * self.b

cpdef long f2(self):
return self.a * self.b


import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(11, 22)
print(a.f2()) # 242
a.f1()
"""
a.f1()
AttributeError: 'cython_test.A' object has no attribute 'f1'
"""

cdef 和 cpdef 之间在函数上的差异,在方法中得到了同样的体现。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cdef class A:

cdef public:
long a, b

def __init__(self, a, b):
self.a = a
self.b = b

cpdef long f2(self):
return self.a * self.b


def func(self_lst):
s = 0
for self in self_lst:
s += self.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.func([a1, a2, a3])) # 16

这是 Python 的特性,一切都是对象,尽管没有指明 self_lst 是什么类型,但只要它可以被 for 循环即可;尽管没有指明 self_lst 里面的元素是什么类型,只要它有 f2 方法即可。并且这里的 func 可以在 Cython 中定义,同样可以在 Python 中定义,这两者是没有差别的,因为都是 Python 中的函数。另外在遍历的时候仍然需要确定这个列表里面的元素是什么,意味着列表里面的元素仍然是 PyObject *,它需要获取类型、转化、属性查找,因为 Cython 不知道类型是什么、导致其无法优化。但如果规定了类型,那么再调用 f2 的时候,那么会直接指向 C 一级的数据结构,因此不需要那些无用的检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cdef class A:

cdef public:
long a, b

def __init__(self, a, b):
self.a = a
self.b = b

cpdef long f2(self):
return self.a * self.b


# 规定接收一个 list,返回一个 long, 它们都是静态的,总之静态类型定义越多速度会越快
cpdef long func(list self_lst):
# 声明 long 类型的 s,A 类型的 self
# 我们下面使用的是 s = s + self.f2(), 所以这里的s要赋一个初始值0
cdef long s = 0
cdef A self
for self in self_lst:
s += self.f2()
return s

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

这个版本的速度比之前快了 10 倍,这表明类型化比非类型化要快了 10 倍。如果删除了 cdef A self,也就是不规定其类型,而还是按照 Python 的语义来调用,那么速度仍然和之前一样,即便使用 cpdef 定义。所以重点在于指定类型为静态类型,只要规定好类型,那么就可以提升速度;而 Cython 是为 Python 服务的,肯定要经常使用 Python 的类型,那么提前规定好、让其指向 C 一级的数据结构,速度会提升很多。如果是 int 和 float,那么就使用 C 中的 long 和 double,这样速度就更加快速了,当然即便用 Python 的 int 和 float 依旧可以起到加速的效果,只不过没有C明显。因此重点是一定要静态定义类型,只要类型明确那么就能进行大量的优化。

Python 慢有很多原因,其中一个原因就是它无法对类型进行优化,以及对象分配在堆上。无法基于类型进行优化,就意味着每次都要进行大量的检测,当然这些前面已经说过了,如果规定好类型,那么就不用兜那么大圈子了;而对象分配在堆上这是无法避免的,只要你用 Python 的对象,都是分配在堆上,所以对于整型和浮点型,通过定义为 C 的类型使其分配在栈上,能够更加的提升速度。总之记住一句话:Cython 加速的关键就在于,类型的静态声明,以及对整数和浮点使用 C 中 long 和 double。

当然,虽说如此,但是该使用 Python 中对象就使用 Python 的对象,基于类型优化其实是可以获得相当可观的速度的。至于要不要通过C的类型(比如使用结构体、共同体这种复杂类型)进行更深一步的优化,就看你对 Cython 的掌握程度了。

在上面的基础上,如果将 cpdef 改成 cdef 那么效率会再次提升,原因很简单,因为 def 和 cpdef 都是支持外部 Python 访问的;而 cdef 只支持内部 Cython 访问,那么它就只指向了一个 C 级的数据结构,但是 def 和 cpdef 都涉及到 Python 函数,而我们说 Python 函数比 C 函数开销要大的。当然 cdef 的缺点就是外部无法访问,而且函数调用需要的开销基本可以忽略不计的。

4.1 方法中给参数指定类型

无论是 def、cdef、cpdef,都可以给参数规定类型,如果类型传递的不对就会报错。比如:上面的 func 函数如果是普通的 Python 函数,那么内部的参数对于 Python 而言只要能够被 for 循环即可,所以它可以是列表、元组、集合。但是上面的 func 规定了类型,参数只能传递 list 对象或者其子类的实例对象,如果传递 tuple 对象就会报错。

然后我们来看看__init__

1
2
3
4
5
6
7
8
cdef class A:

cdef public:
long a, b

def __init__(self, float a, float b):
self.a = a
self.b = b

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

然后为了更好地解释 Cython 带来的性能改进,需要了解关于继承、子类化、和扩展类型的多态性的基础知识。

5、继承和子类化

扩展类型只能继承单个基类,并且继承的基类必须是直接指向 C 实现的类型(可以是使用 cdef class 定义的扩展类型,也可以是内置类型,因为内置类型也是直接指向 C 一级的结构)。如果基类是常规的 Python 类(需要在运行时经过解释器动态解释才能指向 C 一级的结构),或者继承了多个基类,那么 Cython 在编译时会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cdef class Girl:
cdef public:
str name
long age

def __init__(self, name, age):
self.name = name
self.age = age

cpdef str get_info(self):
return f"name: {self.name}, age: {self.age}"


cdef class CGirl(Girl):

cdef public str where

def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)


class PyGirl(Girl):

def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)

定义了一个扩展类(Girl),然后让另一个扩展类(CGirl)和普通的 Python 类(PyGirl)都去继承它。扩展类不可以继承 Python 类,但 Python 类是可以继承扩展类的

1
2
3
4
5
6
7
8
9
10
11
12
13
import pyximport
pyximport.install(language_level=3)

import cython_test

c_girl = cython_test.CGirl("cython", 17, "python")
print(c_girl.get_info()) # name: cython, age: 17

py_girl = cython_test.PyGirl("cython", 17, "python")
print(py_girl .get_info()) # name: cython, age: 17

print(c_girl.where) # python
print(py_girl.where) # python

对于扩展类和普通的 Python 类,它们都是可以继承扩展类的。

继承的话,会有什么样的结果呢?cdef定义的方法和函数一样,无法被外部的Python访问,那么内部的Python类在继承的时候可不可以访问呢?以及私有属性呢?

先来看看 Python 中关于私有属性的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A:

def __init__(self):
self.__name = "xxx"

def __foo(self):
return self.__name


class B(A):

def test(self):
try:
self.__name
except Exception as e:
print(e)

try:
self.__foo()
except Exception as e:
print(e)

B().test()
"""
'B' object has no attribute '_B__name'
'B' object has no attribute '_B__foo'
"""

定义的私有属性只能在当前类里面使用,一旦出去了就不能够再访问了。其实私有属性本质上只是 Python 给你改了个名字,在原来的名字前面加上一个 _类名,所以 __name__foo其实相当于是 _A__name_A__foo。但是当在外部用实例属性去获取__name__foo的时候,获取的就是__name__foo,而显然 A 里面没有这两个属性,因此报错。解决的办法就是通过调用_A__name_A__foo,但是不建议这么做,因为这是私有变量,如果非要访问的话,那就不要定义成私有的。如果是在 A 这个类里面调用的话,那么 Python 解释器也会自动为我们加上 _类名 这个前缀,在类里面调用 self.__name的时候,实际上调用的也是self._A__name 私有属性,但是在外部就不会了。

1
2
3
4
5
6
7
8
9
10
 _A__name = "cython"


class A:

def __init__(self):
self.name = __name

# 是不是很神奇呢? 因为在类里面, __name 等价于 _A__name
print(A().name) # cython

如果是继承的话,通过报错信息也知道原因。B也是一个类,那么在 B 里面调用私有属性,同样会加上 _类名 这个前缀,但是这个类名显然是 B 的类名,不是 A 的类名,因此找不到 _B__name_B__foo,当然我们强制通过_A__name_A__foo也是可以访问的,只是不建议这么做。

因此 Python 中不存在绝对的私有,只不过是解释器内部偷梁换柱将你的私有属性换了个名字罢了,但是可以认为它是私有的,因为按照原本的逻辑没有办法访问。同理继承的子类,也没有办法使用父类的私有属性。

但是在 Cython 中是不是这样子呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
cdef class Person:
cdef public:
long __age
str __name
long length

def __init__(self, name, age, length):
self.__age = age
self.__name = name
self.length = length

cdef str __get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"

cdef str get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"


cdef class CGirl(Person):
cpdef test1(self):
print(self.__name, self.__age, self.length)

cpdef test2(self):
print(self.__get_info())


class PyGirl(Person):

def test1(self):
print(self.length)
print(self.__name, self.__age)

def test2(self):
print(self.__get_info())

def test3(self):
print(self.get_info())

>>> import cython_test
>>> c_g = cython_test.CGirl("古明地觉", 17, 156)
>>> c_g.test1()
古明地觉 17 156
>>> c_g.test2()
name: 古明地觉, age: 17, length: 156
>>>
>>> py_g = cython_test.PyGirl("古明地觉", 17, 156)
>>> py_g.test1()
156
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cython_test.pyx", line 31, in cython_test.PyGirl.test1
print(self.__name, self.__age)
AttributeError: 'PyGirl' object has no attribute '_PyGirl__name'
>>> py_g.test2()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cython_test.pyx", line 34, in cython_test.PyGirl.test2
print(self.__get_info())
AttributeError: 'PyGirl' object has no attribute '_PyGirl__get_info'
>>> py_g.test3()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cython_test.pyx", line 37, in cython_test.PyGirl.test3
print(self.get_info())
AttributeError: 'PyGirl' object has no attribute 'get_info'
>>>

看到对于 Cython 定义的 C 一级的类而言,私有属性、私有方法可以一并使用。但是对于纯 Python 类就不行了,私有属性、私有方法 无法访问就算了,就连父类使用 cdef 定义的非私有方法也无法继承下来,原因就是 PyGirl 是一个 Python 类,不是使用 cdef class 定义的静态类。如果把父类的 cdef get_info 改成 def 或者 cpdef,那么Python子类是可以直接访问的。

我们说 cdef 定义的是 C 一级的方法,不是 Python 的方法、也不是 cpdef 定义的时候自带 Python 包装器,因此它无法被 Python 子类继承,因此它并没有跨越语言的边界。当然如果不熟悉 Cython 中的继承、并且有很想使用继承,那么就不要使用 cdef,直接使用 cpdef 定义吧(或者使用 def,只不过此时无法指定返回值类型)。虽说 cdef 只定义的 C 一级的函数调用比自带 Python 包装器的 cpdef 快,但是说实话那一点点快几乎没啥意义。Cython 加速的核心在于类型上的优化,如果我们能使用静态的方式声明,那么速度就会有明显的提升,不要为了加速反倒畏手畏脚地这不敢用那不敢用。

总之 Cython 加速记住两个原则:

  • 能使用静态声明的方式使用静态声明,不仅是变量,还有参数、返回值;
  • 这是 Cython 默认的行为,int 和 float 使用的是 C 中 的int 和 float,但是为了支持更大的数字,我们直接使用 long 和 double 即可。

但是问题来了,如果希望自定义的扩展类不可以被其它类继承的话该怎么做呢?

1
2
3
4
5
6
cimport cython

# 通过 cython.final 进行装饰,那么这个类就不可被继承了
@cython.final
cdef class NotInheritable:
pass

通过 cython.final,那么被装饰的类就是一个不可继承类,不光是外界普通的 Python 类,内部的扩展类也是不可继承的。

1
2
3
4
5
6
7
8
9
10
import pyximport
pyximport.install(language_level=3)

import cython_test

class A(cython_test.NotInheritable):
pass
"""
TypeError: type 'cython_test.NotInheritable' is not an acceptable base type
"""

告诉我们 NotInheritable 不是一个合法的基类。

6、类型转换

Python 中类在继承扩展类的时候,无法继承其内部的 cdef 方法,但如果这个类是继承扩展类的,那么其实例对象可不可以转化为扩展类的类型呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cdef class A:

cdef funcA(self):
return 123

class B(A):
# 显然 func1 内部无法访问扩展类A的funcA
def func1(self):
return self.funcA()

# 但是我们在使用的时候将其类型转化一下
def func2(self):
return (<A> self).funcA()

>>> import cython_test
>>> b = cython_test.B()
>>> b.func1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cython_test.pyx", line 10, in cython_test.B.func1
return self.funcA()
AttributeError: 'B' object has no attribute 'funcA'
>>> b.func2()
123
>>>

看到 b.func2 是可以调用成功的,但知道使用 <> 这种方式如果转化不成功,那么也不会有任何影响,会保留原来值(C中的整型和浮点除外),这可能会有点危险。因此可以通过 (<A?> self),这样 self 必须是 A 或者其子类的实例对象,否则报错。

7、扩展类型对象和None

看一个简单的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cdef class Girl:
cdef public:
str name
long age

def __init__(self, name, age):
self.name = name
self.age = age

cpdef tuple dispatch(Girl g):
return g.name, g.age

import pyximport
pyximport.install(language_level=3)

import cython_test

print(cython_test.dispatch(cython_test.Girl("python", 17))) # ('python', 17)
print(cython_test.dispatch(cython_test.Girl("cython", 16))) # ('cython', 16)

class B(cython_test.Girl):
pass

print(cython_test.dispatch(B("mashiro", 16))) # ('mashiro', 16)

cython_test.dispatch(object())
"""
cython_test.dispatch(object())
TypeError: Argument 'g' has incorrect type (expected cython_test.Girl, got object)
"""

传递一个 Girl 或者其子类的实例对象的话是没有问题的,但是传递一个其它的则不行。

但是在 Cython 中 None 是一个例外,即使它不是 Girl 的实例对象,但也是可以传递的,除了 C 规定的类型之外,只要是 Python 的类型,不管什么,传递一个 None 都是可以的。这就类似于 C 中的空指针,任何指针都可以传递给空指针,但是没有办法做什么操作。

所以这里可以传递一个 None,但是执行逻辑的时候显然会报错。

1
2
3
4
5
import pyximport
pyximport.install(language_level=3)

import cython_test
cython_test.dispatch(None)

然而报错还是轻的,上面代码执行的时候会发生段错误,解释器直接异常退出了。原因就在于不安全地访问了 Girl 实例对象的成员属性,属性和方法都是 C 接口的一部分,而 Python 中 None 本质上没有 C 接口,因此访问属性或者调用方法都是无效的。为了确保这些操作的安全,最好加上一层检测。

1
2
3
4
cpdef tuple dispatch(Girl g):
if g is None:
raise TypeError("...")
return g.name, g.age

但是除了上面那种做法,Cython 还提供了一种特殊的语法。

1
2
def dispatch(Girl g not None):
return g.name, g.age

此时如果我们传递了 None,那么就会报错。不过这个版本由于要预先进行类型检查,判断是否为 None,从而会牺牲一些效率。不过虽说如此,但是传递 None 所造成的段错误是非常致命的,因此非常有必要防范这一点的。当然还是那句话,虽然效率会牺牲一点点,但还是那句话,与 Cython 带来的效率提升相比,这点牺牲是非常小的,况且这也是必要的。但是注意:not None 只能出现在 def 定义的函数中,cdef 和 cpdef 是不合法的

1
2
3
4
5
6
7
8
9
import pyximport
pyximport.install(language_level=3)

import cython_test
cython_test.dispatch(None)
"""
cython_test.dispatch(None)
TypeError: Argument 'g' has incorrect type (expected cython_test.Girl, got NoneType)
"""

此时对 None 也是一视同仁的,传递一个 None 也是不符合类型的。这里我们设置的是 not None,但是除了 None 还能设置别的吗?答案是不行的,只能设置 None,因为 Cython 只有对 None 不会进行检测。

1
2
3
4
5
 cpdef tuple dispatch(Girl g not 123):
^
------------------------------------------------------------

cython_test.pyx:11:24: Expected 'None'

许多人认为需要 not None 字句是不方便的,这个特性经常被争论,但幸运的是,在函数的参数声明中使用 not None 是非常方便的。

为了更高的性能,Cython 还提供了一个默认的 nonecheck 编译器指令,可以对整个扩展模块不进行检查,通过在文件的开头加上一个注释:# cython: nonecheck=True

8、Cython中扩展类的property

Python 中的 property 非常的易用且强大,可以精确地控制某个属性的修改,而 Cython 也是支持 property 描述符的,但是方式有些不一样。不过在介绍 Cython 的 property 之前,来看看 Python 中的 property。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Girl:

def __init__(self):
self.name = None

@property
def x(self):
# 不需要我们对x进行调用,直接通过self.x即可获取返回值
# 让函数像属性一样直接获取
return self.name

@x.setter
def x(self, value):
# 当我们self.x = "cython"的时候,会调用这个函数
# "cython"就会传递给这里的value
self.name = value

@x.deleter
def x(self):
# 执行del self.x的时候,就会调用这个函数
print("被调用了")
del self.name


girl = Girl()
print(girl.x) # None
girl.x = "cython"
print(girl.x) # cython
del girl.x # 被调用了

这里是通过装饰器的方式实现的,三个函数都是一样的名字,除了使用装饰器,还可以这么做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Girl:

def __init__(self):
self.name = None

def fget(self):
return self.name

def fset(self, value):
self.name = value

def fdel(self):
print("被调用了")
del self.name

# 传递三个函数即可,除此之外还有一个doc属性
x = property(fget, fset, fdel, doc="这是property")

girl = Girl()
print(girl.x) # None
girl.x = "cython"
print(girl.x) # cython
del girl.x # 被调用了

所以 property 就是像访问属性一样访问函数,那么它内部是怎么做到的呢?不用想,肯定是通过描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class MyProperty:  # 模仿类property,实现与其一样的功能
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.doc = doc

def __get__(self, instance, owner):
return self.fget(instance)

def __set__(self, instance, value):
return self.fset(instance, value)

def __delete__(self, instance):
return self.fdel(instance)

def setter(self, func):
return type(self)(self.fget, func, self.fdel, self.doc)

def deleter(self, func):
return type(self)(self.fget, self.fset, func, self.doc)

class Girl1:

def __init__(self):
self.name = None

@MyProperty
def x(self):
return self.name

@x.setter
def x(self, value):
self.name = value

@x.deleter
def x(self):
print("被调用了")
del self.name

class Girl2:

def __init__(self):
self.name = None

def fget(self):
return self.name

def fset(self, value):
self.name = value

def fdel(self):
print("被调用了")
del self.name

x = MyProperty(fget, fset, fdel)


girl1 = Girl1()
print(girl1.x) # None
girl1.x = "cython"
print(girl1.x) # cython
del girl1.x # 被调用了

girl2 = Girl2()
print(girl2.x) # None
girl2.x = "cython"
print(girl2.x) # cython
del girl2.x # 被调用了

通过描述符的方式手动实现了一个 property 的功能,描述符事实上在 Python 解释器的层面也用的非常多,实例调用方法的时候,第一个参数 self 会自动传递也是通过描述符实现的。所以描述符不光在 Python 的层面用,在解释器的层面上也大量使用描述符。同理字典也是如此,定义的类的实例对象的属性都是存在一个字典里面的,称之为属性字典,所以字典在 Python 中是经过高度优化的,原因就是不仅我们在用,底层也在大量使用。

下面来看看Cython中的property

针对扩展类的 property,Cython 有着不同的语法,但是实现了相同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cdef class Girl:
cdef str name

def __init__(self):
self.name = None

property x:
def __get__(self):
return self.name

def __set__(self, value):
self.name = value

import pyximport
pyximport.install(language_level=3)

import cython_test

g = cython_test.Girl()
print(g.x) # None
g.x = "cython"
print(g.x) # cython

看到 Cython 是将 property 和描述符结合在一起了,但是实现起来感觉更方便了。

不过最重要的还是魔法方法,魔法方法算是 Python 中非常强大的一个特性, Python 将每一个操作符都抽象成了对应的魔法方法,也正因为如此 numpy 也得以很好的实现。那么在 Cython 中,魔法方法是如何体现的呢?

9、魔法方法在Cython中使用

通过魔法方法可以对运算符进行重载,魔法方法的特点就是它的函数名以双下划线开头、并以双下划线结尾。之前讨论了__cinit____init____dealloc__,并了解了它们分别用于 C 一级的初始化、Python 一级的初始化、对象的释放(特指 C 中的指针)。除了那三个,Cython 中也支持其它的魔法方法,但是注意:Cython 不支持 __del____del____dealloc__负责实现。

9.1 算术魔法方法

假设在 Python 中定义了一个类 class A,如果希望 A 的实例对象可以进行加法运算,那么内部需要定义__add____radd__方法。关于__add____radd__的区别就在于该实例对象是在加号的左边还是右边。以 A() + B() 为例,A 和 B 是我们自定义的类:

  • 首先尝试寻找 A 的 __add__ 方法, 如果有直接调用
  • 如果 A 中不存在 __add__ 方法, 那么会去寻找 B 的 __radd__ 方法

但如果是一个整数和自定义的类的实例对象相加呢?

  • 123 + A(): 先寻找 A 的 __radd__
  • A() + 123: 先寻找 A 的 __add__

代码演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A:

def __add__(self, other):
return "A add"

def __radd__(self, other):
return "A radd"

class B:

def __add__(self, other):
return "B add"

def __radd__(self, other):
return "B radd"


print(A() + B()) # A add
print(B() + A()) # B add
print(123 + B()) # B radd
print(A() + 123) # A add

除了类似于 __add__这种实例对象放在左边、__radd__这种实例对象放在右边,还有__iadd__,它用于 += 这种形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A:

def __iadd__(self, other):
print("__iadd__ is called")
return 1 + other

a = A()
a += 123
print(a)
"""
__iadd__ is called
124
"""
# 如果没定义__iadd__,也是可以使用这种形式,会转化成a = a + 123,所以会调用__add__方法

当然这都比较简单,其它的算数魔法方法也是类似的。并且里面的 self 就是对应类的实例对象,有人会觉得这不是废话吗?之所以要提这一点,是为了给下面的Cython做铺垫。

对于 Cython 中的扩展类来说,不使用类似于 __radd__这种实现方式,只需要定义一个 __add__即可同时实现 __add____radd__。对于 Cython 中的扩展类型 A,a 是 A 的实例对象,如果是 a + 123,那么会调用 __add__方法,然后第一个参数是 a、第二个参数是123;但如果是 123 + a,那么依旧会调用__add__,不过此时 __add__的第一个参数是 123、第二个参数才是 a。所以不像 Python 中的魔法方法,第一个参数 self 永远是实例本身,第一个参数是谁取决于谁在前面。所以将第一个参数叫做 self 容易产生误解,官方也不建议将第一个参数使用 self 作为参数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
cdef class Girl:

def __add__(x, y):
return x, y

import pyximport
pyximport.install(language_level=3)

import cython_test

g = cython_test.Girl()
print(g + 123) # (<cython_test.Girl object at 0x0000028752477940>, 123)
print(123 + g) # (123, <cython_test.Girl object at 0x0000028752477940>)

我们看到,__add__中的参数确实是由位置决定的,那么再来看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cdef class Girl:
cdef long a

def __init__(self, a):
self.a = a

def __add__(x, y):
if isinstance(x, Girl):
# 这里为什么需要转化呢?直接 x.a + y 不行吗?
# 答案是不行的,因为这个 x 是我们外部传过来的 Girl 对象
# 但是我们这里的 a 不是一个 public 或者 readonly,直接访问是得不到的,所以需要转化一下才可以访问
return (<Girl> x).a + y
return (<Girl> y).a + x

import pyximport
pyximport.install(language_level=3)

import cython_test

g = cython_test.Girl(3)
print(g + 2) # 5
print(2 + g) # 5

# 和浮点数运算也是可以的
print(g + 2.1) # 5.1
print(2.1 + g) # 5.1

g += 4
print(g) # 7

除了 __add__,Cython 也是支持__iadd__ 的,此时的第一个参数是 self,因为 += 这种形式,第一种参数永远是实例对象。

9.2 富比较

Cython 的扩展类可以使用__eq__ne__等等,和 Python 一致的富比较魔法方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cdef class A:
# 这里比较操作符两边的值的位置依旧会影响这里的x、y
# 但是对于Python中的比较来说则不会,self永远是实例对象
def __eq__(self, other):
print(self, other)
return "=="

import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A()
print(a == 3)
"""
<cython_test.A object at 0x0000015D792C7940> 3
==
"""
print(3 == a)
"""
<cython_test.A object at 0x0000015D792C7940> 3
==
"""

和算术魔法方法不一样,比较操作没有__req__或者__ieq__,并且比较的时候第一个参数永远是实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
cdef class A:

def __eq__(self, other):
print(self, other)
return "A =="

class B:

def __eq__(self, other):
print(self, other)
return "B =="

import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A()
b = cython_test.B()
# 调用 a 的 __eq__
print(a == 123)
"""
<cython_test.A object at 0x00000223641631C0> 123
A ==
"""
# 调用 b 的 __eq__
print(b == 123)
"""
<cython_test.B object at 0x00000223641E71F0> 123
B ==
"""
# 调用 a 的 __eq__, 第一个参数还是 a
print(a == 123)
"""
<cython_test.A object at 0x00000223641631C0> 123
A ==
"""
# 调用 b 的 __eq__, 第一个参数还是 b
print(b == 123)
"""
<cython_test.B object at 0x00000223641E71F0> 123
B ==
"""
# 调用 a 的 __eq__, 第一个参数是 a, 第二个参数是 b
print(a == b)
"""
<cython_test.A object at 0x00000223641631C0> <cython_test.B object at 0x00000223641E71F0>
A ==
"""
# 调用 b 的 __eq__, 第一个参数是 b, 第二个参数是 a
print(b == a)
"""
<cython_test.B object at 0x00000223641E71F0> <cython_test.A object at 0x00000223641631C0>
B ==
"""

链式比较也是可以的,比如:a == b == 123 等价于 a == b and b == 123。

1
2
3
4
5
6
7
8
9
10
11
12
13
import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A()
b = cython_test.B()
print(a == b == 123)
"""
<cython_test.A object at 0x000001817F1D31C0> <cython_test.B object at 0x000001817630E0D0>
<cython_test.B object at 0x000001817630E0D0> 123
B ==
"""

先执行 a == b 返回 “A ==”,再执行 b == 3 返回 “B ==”,然后 “A ==” 和 “B ==” 进行 and,前面为真,所以返回后面的 “B ==”。

9.3 迭代器支持

Cython 中的扩展类也是支持迭代器协议的,而且定义的方法和纯 Python 之间是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cdef class A:

cdef public:
list values
long __index

def __init__(self, values):
self.values = values
self.__index = 0

def __iter__(self):
return self

def __next__(self):
try:
ret = self.values[self.__index]
self.__index += 1
return ret
except IndexError:
raise StopIteration
import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(['椎名真白', '古明地觉', '雾雨魔理沙'])
for _ in a:
print(_)
"""
椎名真白
古明地觉
雾雨魔理沙
"""

知道在 Python 中,for 循环会先去寻找__iter__,但如果找不到会退而求其次去找 __getitem__,那么在 Cython 中是不是也是如此呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cdef class A:

cdef public:
list values
long __index

def __init__(self, values):
self.values = values
self.__index = 0

def __getitem__(self, item):
return self.values[item]

import pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A(['椎名真白', '古明地觉', '雾雨魔理沙'])
for _ in a:
print(_)
"""
椎名真白
古明地觉
雾雨魔理沙
"""

我们看到,也是一样的。

当然上面只是介绍了魔法方法的一部分,Python 中的魔法方法(比如__getattr____call____hash__等等)在 Cython 中基本上都支持,并且 Cython 还提供了一些 Python 所没有的魔法方法。当然这些我们就不说了,如果你熟悉 Python 的话,那么在 Cython 中也是按照相同的方式进行使用即可。总之,用久了就孰能生巧了。

注意:魔法方法只能用def定义,不可以使用cdef或者cpdef。

还有上下文管理器,在Cython中也是一样的用法。Python中基本上所有的魔法方法在Cython都可以直接用。

1
2
3
4
5
6
7
8
9
10
11
12
mport pyximport
pyximport.install(language_level=3)

import cython_test

a = cython_test.A()
with a:
pass
"""
__enter__
__exit__
"""

Cython 中的扩展类,它和 Python 中内置类是等价的,都是直接指向了 C 一级的数据结构,不需要字节码的翻译过程。也正因为如此,它失去一些动态特性,但同时也获得了效率,因为这两者本来就是不可兼得的。

Cython 的类有点复杂,还是需要多使用,不过它毕竟在各方面都和 Python 保持接近,因此学习来也不是那么费劲。

虽然创建扩展类的最简单的方式是通过 Cython,但是通过 Python/C API 直接在 C 中实现的话,则是最有用的练习,但还是那句话,它需要我们对 Python/C API 有一个很深的了解,而这是一个非常难得的事情,因此使用 Cython 就变成了我们最佳的选择。