《Cython系列》4. Cython中的扩展类

Posted traditional

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Cython系列》4. Cython中的扩展类相关的知识,希望对你有一定的参考价值。

楔子

在之前的博客中,我们介绍了Cython给Python添加的一些基础特性,以及它们所提供的功能和用法。但是Cython还可以增强Python的类,不过在学习之前我们需要了解一下Python的类和扩展类之前的区别。

Python类和扩展类之间的差异

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

在这一节,我们将会学习如何使用Cython操纵Python中的类。

首先Python中内置了很多的类,tuple、dict等等,这些类在C一级通过Python/C api直接包含在了Python运行时中。但就使用而言,它和我们自己使用class定义的类是没有什么区别的,如果非要说区别的话,那就是内置的类的一些属性、以及内置的类里面的方法的属性是没法修改的。当然我们删除一个属性、添加一个属性也是不可以的。

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出现在此的原因:Cython使得我们创建和操作扩展类就像操作Python中的类一样。在Cython中定义一个扩展类通过cdef class的形式,和Python中的常规类保持了高度的相似性。

尽管在语法上有着相似之处,但是cdef class定义的类对所有方法和数据都有快速的C级别的访问,这也是和扩展类和Python中的普通类之间的一个最显著的区别。

Cython中的扩展类

举一个Python中类

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

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

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来说,定义的类是可以被外部访问的,虽然函数不行、但类可以。

>>> import cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.get_area()
12
>>> 
>>> rect = cython_test.Rectangle("3", "4")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "1.pyx", line 6, in cython_test.Rectangle.__init__
    self.width = w
TypeError: an integer is required
>>> # 我们传递了一个字符串,告诉我们需要一个整型

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

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 cython_test
>>> rect = cython_test.Rectangle(3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "1.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内置类型的例子吧

a = 1
try:
    a.xx = 123
except Exception as e:
    print(e)  # ‘int‘ object has no attribute ‘xx‘

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

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 cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.get_area()
12
>>> 
>>> rect.a = "xx"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘a‘
>>> # 如果想动态修改、添加类型,那么需要解释器在解释的时候来动态操作
>>> # 但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力
>>> # 也正因为如此,也能提高效率。因为很多时候,我们不需要动态修改。
>>>
>>> # 当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里
>>> # 其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr等价于instance.__dict__["attr"],同理修改、创建也是。
>>> # 但是注意:这只是针对普通的Python类而言,但扩展类内部是没有__dict__的。
>>> rect.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘__dict__‘
>>>
>>>
>>> # 不光没有__dict__,你连self本身的属性都无法访问
>>> rect.width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘width‘
>>> # 提示我们self没有width属性,所以我们实例化之后再想修改是不行的,连获取都获取不到
>>> # 只能调用它的一些方法罢了。

对于内建的类,其实例对象也是没有属性字典的。

a = 123
try:
    print(a.__dict__)
except Exception as e:
    print(e)  # ‘int‘ object has no attribute ‘__dict__‘

# 但是int这个类本身是有属性字典的,只是没有办法赋值
# 而我们自定义的Python类是可以这么做的,等于给类添加了一些函数
try:
    int.__dict__["xx"] = "xx"
except Exception as e:
    print(e)  # ‘mappingproxy‘ object does not support item assignment
# 还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候是允许你做一些这样的骚操作的
# 但是内置的类和扩展类是不需要解释这一步的,它们是彪悍的人生,直接指向了C一级的数据结构,因此也就丧失了这种动态的能力

同理对于扩展类,也是相同的结果。

>>> cython_test.Rectangle.__dict__["xx"] = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ‘mappingproxy‘ object does not support item assignment
>>> 

所以扩展类和内置的类的表现是相同的。

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

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 cython_test
>>> rect = cython_test.Rectangle(3, 4)
>>> rect.width
3
>>> rect.get_area()
12
>>> rect.width = 123
>>> rect.get_area()
492
>>> 
>>> rect.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.Rectangle‘ object has no attribute ‘__dict__‘
>>> # 但属性字典依旧是没有的

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

>>> rect.width
3
>>> rect.width = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute ‘width‘ of ‘cython_test.Rectangle‘ objects is not writable
  • cdef readonly 类型 变量名:实例属性可以被访问,但是不可以被修改
  • cdef public 类型 变量名:实例属性可以被访问,也可以被修改
  • cdef 类型 变量名:实例属性既不可以被访问,更不可以被修改

当然即便使用cdef public和cdef readonly定义变量,Cython也一样可以实行快速访问,因为扩展类的方法基本上忽略了readonly和public的声明,它们存在的目的只是为了控制来自Python的访问。

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

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

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

# 导入相关函数,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):
        """如果进行了动态内存分配,那么必须要定义__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 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__

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 cython_test
>>> a = cython_test.A(111, 222)
__cinit__
(111, 222)
__init__
(111, 222)
>>> a.a
111
>>> a.b
222
>>>

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

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__,所以参数一般都写*args, **kwargs
        但是如果分配的内存如果需要通过参数来指定的话,那么还是不建议使用*args和**kwargs的
        具体情况具体分析
        """
        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)

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的。

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 cython_test
>>> a = cython_test.A(11, 22)
>>> a.f2()
242
>>> a.f1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘cython_test.A‘ object has no attribute ‘f1‘
>>> 

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

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

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 cython_test
>>> a1 = cython_test.A(1, 2)
>>> a2 = cython_test.A(2, 4)
>>> a3 = cython_test.A(2, 3)
>>> 
>>> cython_test.func([a1, a2, a3])
16
>>> 

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

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,参数也变成静态变量的话会更快,总之静态定义越多速度会越快
def 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

调用得到的结果是一样的,可以自己尝试一下。这样的话速度会变快很多,因为我们实例对象静态类的实例对象,f2方法也是cpdef方法,所以在执行的时候不涉及Python对象。并且求和的时候,也是一个指使用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的掌握程度了。

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

方法中给参数指定类型

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

当然,对于__cinit____init__也是可以的,另外我们说这两位老铁只能用def定义。

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带来的性能改进,我们需要了解关于继承、子类化、和扩展类型的多态性的基础知识。

继承和子类化

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

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):
        # 在Cython中我们是可以使用了f-string的
        # 但是返回值类型我们除了使用str,还可以要使用unicode,这个我们后面还会说
        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类是可以继承扩展类的。

>>> import cython_test
>>> 
>>> c_girl = cython_test.CGirl("古明地觉", 17, "东方地灵殿")
>>> c_girl.get_info()
‘name: 古明地觉, age: 17‘
>>> 
>>> py_girl = cython_test.PyGirl("古明地觉", 17, "东方地灵殿")
>>> py_girl.get_info()
‘name: 古明地觉, age: 17‘
>>> 
>>> c_girl.where
‘东方地灵殿‘
>>> py_girl.where
‘东方地灵殿‘
>>> 

我们看到,对于扩展类和普通的Python类,它们都是可以继承扩展类的。这里我们的name和where没有使用char *而是使用的str,我个人建议不使用char *,原因有两个:

  • 1. 传递的时候需要手动编码成bytes
def func(char *name):
    print(name)
>>> import cython_test
>>> cython_test.func("古明地觉")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "1.pyx", line 1, in cython_test.func
    def func(char *name):
TypeError: expected bytes, str found
>>> 
>>> cython_test.func("古明地觉".encode("utf-8"))
b‘xe5x8fxa4xe6x98x8exe5x9cxb0xe8xa7x89‘
>>> 

提示我们:需要一个bytes,但是却传递了一个str。问题是我们之前说一个单独的字符串可以被解释成C中的字符串啊,那只是在Cython中声明一个普通变量的时候(比如:cdef char *name="古明地觉"),可以当成普通的字符串,如果是作为参数那么是不行的,必须手动编码成字节。

  • 2. 在类中会出现意想不到的结果
cpdef char *func1():
    # 直接返回一个bytes
    return b"xxx"


cpdef char *func2():
    # 赋值给char *,返回这个char *变量
    cdef char *res = "yyy"
    return res


cpdef char *func3(char *name):
    # 返回name本身
    return name


cpdef char *func4(char *name):
    # 先将name解码成Python的字符串,进行f-strings,然后编码成字节返回
    return f"name: {name.decode(‘utf-8‘)}".encode("utf-8")
>>> import cython_test
>>> 
>>> cython_test.func1()
b‘xxx‘
>>> 
>>> cython_test.func2()
b‘yyy‘
>>> cython_test.func3("古明地觉".encode("utf-8"))
b‘xe5x8fxa4xe6x98x8exe5x9cxb0xe8xa7x89‘
>>> 
>>> cython_test.func3("古明地觉".encode("utf-8")).decode("utf-8")
‘古明地觉‘
>>> 
>>> cython_test.func4("古明地觉".encode("utf-8")).decode("utf-8")
‘name: 古明地觉‘

我们看到对于函数来说,是没有任何问题的,将cpdef换成def也是一样的, 但是换成类呢?

cdef class Girl:

    cdef public char *name1

    def __init__(self, name1):
        self.name1 = name1
	
    # 这里使用普通的Python函数
    def foo1(self, char* name2):
        print(self.name1.decode("utf-8"))
        print(name2.decode("utf-8"))
    
    def foo2(self):
        print(self.name1.decode("utf-8"))
    
    def foo3(self):
        cdef bytes tmp = self.name1
        print(tmp)
        print(tmp.decode("utf-8"))
>>> import cython_test
>>> g = cython_test.Girl("古明地觉".encode("utf-8"))
>>> g.foo1("椎名真白".encode("utf-8"))
椎名真白
椎名真白
>>> g.foo2()

>>> g.foo3()
b‘‘
 
>>> 

惊了,我们看到在foo1中self.name1和name2在decode之后得到的都是name2的结果;在foo2中,self.name1根本打印不出来;在foo3中,即便我们赋值给了一个变量,还是得不到正确的结果。

显然我们看到方法foo1中传递的参数是可以正常打印的,不能正常打印的是self的char *属性,但是关于原因,为什么在函数中可以正常使用,但到了类的实例对象这里就不行了,这个我目前也无法解释。书中使用的是数值,我没有完全按照它的举的例子,因此这个结果也是我偶然发现的。将字符串声明为char *的时候,发现了这个问题。显然这已经超出了我可以调试的范畴,这个在不了解Cython底层是如何加速类的情况下,想调试得到答案对我而言是一件不可能完成的任务。

因此我个人建议,像创建变量、if、for、函数、类等等,在这些Python的逻辑中,C的类型只使用long和double即可,其它的就还使用Python的类型,无论是参数还是返回值都是如此。至于像结构体、共同体等C的数据结构中,是否使用char *我们后续再讨论。

除此之外还有一个重点,那就是返回值的问题,我们看到上面的CGirl这个类,我们说的get_info函数除了使用cdef?str,还可以使用cdef?unicode,这在Python3中是没有区别的。但如果你在编译的时候没有指定language_level = 3,那么str会默认使用Python2中的str,那么你使用Python3的时候就必须传递一个字节串了。因为Python2中的str在Python3中代表bytes,如果使用Python2的语义编译并且还想传递字符串的话,那么需要通过unicode,因为Python2的unicode相当于Python3的str。所以我们才说,在编译的时候要显式地指定language_level=3,否则很容易出现这种错误。

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

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

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私有属性,但是在外部就不会了。

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

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

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

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 "1.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 "1.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 "1.pyx", line 37, in cython_test.PyGirl.test3
    print(self.get_info())
AttributeError: ‘PyGirl‘ object has no attribute ‘get_info‘
>>> 

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

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

总之Cython加速记住两个原则:1. 能使用静态声明的方式使用静态声明,不仅是变量,还有参数、返回值;2. 关于int和float,使用C中的long和double。关于优化我们后面还会继续说,总之原则就是上面这两点做到了,我们的目的就达成了。至于cdef比def、cpdef少的那一点点函数调用的开销可以说是沧海一粟,更何况你要想被Python访问,光一个cdef也办不到,肯定需要依赖Python的包装器的。

类型转化

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

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 "1.pyx", line 10, in cython_test.B.func1
    return self.funcA()
AttributeError: ‘B‘ object has no attribute ‘funcA‘
>>> b.func2()
123
>>> 

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

扩展类型对象和None

看一个简单的函数

cdef class Girl:
    cdef public:
        str name
        long age

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


def dispatch(Girl g):
    print(g.name, g.age)
>>> import cython_test
>>> cython_test.dispatch(cython_test.Girl("古明地觉", 17))
古明地觉 17
>>> cython_test.dispatch(cython_test.Girl("椎名真白", 16))
椎名真白 16
>>> class B(cython_test.Girl):
...     pass
... 
>>> cython_test.dispatch(B("mashiro", 16))
mashiro 16
>>> 
>>> cython_test.dispatch(object())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Argument ‘g‘ has incorrect type (expected cython_test.Girl, got object)
>>> 

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

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

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

>>> import cython_test
>>> cython_test.dispatch(None)
Segmentation fault
[root@iz2ze3ik2oh85c6hanp0hmz ~]# 

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

def dispatch(Girl g):
    if g is None:
        raise TypeError("...")
    print(g.name, g.age)

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

def dispatch(Girl g not None):
    print(g.name, g.age)

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

>>> import cython_test
>>> cython_test.dispatch(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Argument ‘g‘ has incorrect type (expected cython_test.Girl, got NoneType)
>>> 

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

def dispatch(Girl g not 123):
                       ^
------------------------------------------------------------

1.pyx:11:24: Expected ‘None‘

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

个人觉得Cython的语法设计的真酷,笔者本人非常喜欢。

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

Cython中扩展类的property

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

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 = "古明地觉"的时候,会调用这个函数
        # "古明地觉"就会传递给这里的value
        self.name = value

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


girl = Girl()
print(girl.x)  # None
girl.x = "古明地觉"
print(girl.x)  # 古明地觉
del girl.x  # 被调用了

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

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 = "古明地觉"
print(girl.x)  # 古明地觉
del girl.x  # 被调用了

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

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 = "古明地觉"
print(girl1.x)  # 古明地觉
del girl1.x  # 被调用了


girl2 = Girl2()
print(girl2.x)  # None
girl2.x = "古明地觉"
print(girl2.x)  # 古明地觉
del girl2.x  # 被调用了

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

下面来看看Cython中的property

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

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 cython_test
>>> g = cython_test.Girl()
>>> 
>>> g.x
>>> print(g.x)
None
>>> g.x = "古明地觉"
>>> g.x
‘古明地觉‘

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

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

魔法方法在Cython中更加魔法

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

算术魔法方法

假设在Python中定义了一个类class A,那么如果希望A的实例对象可以使用+,那么内部需要定义def __add__(self, other)方法。然后a + 123的时候,就会转化成A.__add__(a, 123),其中a是A的实例对象。如果是123 + a的话,那么会先调用123的__add__方法,如果出现类型错误,那么会去检测a是否有__radd__,有的话会去调用,没有的话就会报错,同理对于其它的算术操作也是类似的。

class A:

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

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


a = A()
print(a + 123)
"""
__add__ is called
124
"""
print(123 + a)  # 先调用123的__add__,没有的话调用a的__radd__
"""
__radd__ is called
124
"""
# 当然的例子中,两个魔法方法中的self都是A的实例对象,有人会觉得这不是废话吗
# 之所以要提这一点,是为了给后面的Cython做铺垫

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

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__方法

所以Python真的是把每一个操作都抽象成了一个魔方方法。

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

cdef class Girl:

    def __add__(x, y):
        return x, y
>>> import cython_test
>>> g = cython_test.Girl()
>>> 
>>> g + 123
(<cython_test.Girl object at 0x7f554d2ad120>, 123)
>>> 123 + g
(123, <cython_test.Girl object at 0x7f554d2ad120>)
>>> 

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

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 cython_test
>>> g = cython_test.Girl(3)
>>> g + 2
5
>>> 2 + g
5
>>> # 和浮点运算也是可以的
>>> g + 2.1
5.1
>>> 2.1 + g
5.1
>>> g += 4
>>> g
7
>>> 

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

另外我们这里说的__add____iadd__只是举例,其它的算术操作也是可以的。

富比较

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

cdef class A:

    # 这里比较操作符两边的值的位置依旧会影响这里的x、y
    # 但是对于Python中的比较来说则不会,self永远是实例对象

    def __eq__(self, other):
        print(self, other)
        return "=="
>>> import cython_test
>>> a = cython_test.A()
>>> a == 3
<cython_test.A object at 0x7f87b034a120> 3
‘==‘
>>> 3 == a
<cython_test.A object at 0x7f87b034a120> 3
‘==‘
>>> 

a == 3,那么会调用a的__eq__,3 == a会先调用3的__eq__,如果抛出个类型错误,那么会改用a的__eq__,但是和算术魔法方法不一样,比较操作没有__req__或者__ieq__,并且比较的时候第一个参数永远是实例对象。

cdef class A:

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


class B:

    def __eq__(self, other):
        print(self, other)
        return "B =="
>>> import cython_test
>>> 
>>> a = cython_test.A()
>>> b = cython_test.B()
>>> 
>>> a == 123  # 调用a的__eq__
<cython_test.A object at 0x7f8e1f684120> 123
‘A ==‘
>>> b == 123  # 调用b的__eq__
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘
>>> 
>>> 123 == a  # 调用a的__eq__, 第一个参数还是a
<cython_test.A object at 0x7f8e1f684120> 123
‘A ==‘
>>> 123 == b  # 调用b的__eq__, 第一个参数还是b
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘
>>> 
>>> a == b  # 调用a的__eq__, 第一个参数是a, 第二个参数是b
<cython_test.A object at 0x7f8e1f684120> <cython_test.B object at 0x7f8e177e3080>
‘A ==‘
>>> b == a  # 调用b的__eq__, 第一个参数是b, 第二个参数是a
<cython_test.B object at 0x7f8e177e3080> <cython_test.A object at 0x7f8e1f684120>
‘B ==‘
>>> 

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

>>> a == b == 123
<cython_test.A object at 0x7f8e1f684120> <cython_test.B object at 0x7f8e177e3080>
<cython_test.B object at 0x7f8e177e3080> 123
‘B ==‘

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

然后Python类和扩展类之间的最后一个差别就是对迭代器的支持。

迭代器支持

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

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 cython_test
>>> a = cython_test.A([‘椎名真白‘, ‘古明地觉‘, ‘雾雨魔理沙‘])
>>> for _ in a:
...     _
... 
‘椎名真白‘
‘古明地觉‘
‘雾雨魔理沙‘
>>> 

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

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 cython_test
>>> a = cython_test.A([‘椎名真白‘, ‘古明地觉‘, ‘雾雨魔理沙‘])
>>> for _ in a:
...     _
... 
‘椎名真白‘
‘古明地觉‘
‘雾雨魔理沙‘
>>> 

我们看到,也是一样的。

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

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

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

cdef class A:

    def __enter__(self):
        print("__enter__")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__")
>>> 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就变成了我们最佳的选择。

我们老说Python/C api,可能有人觉得这到底是个什么玩意,别急,最后我们就来演示一下吧。我们不拿类来举例,就拿函数举例吧。因为使用C实现Python的函数就已经够复杂了,我们要实现的函数是:接收两个整型,然后返回两个整型之和。

#include "Python.h"  

//函数的具体实现
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
  int a, b;

  if (!PyArg_ParseTuple(args, "ii", &a, &b)){
    return NULL;
  }
  return PyLong_FromLong(a + b);
}

static PyMethodDef module_functions[] = {
  {
    "my_func1",
    (PyCFunction)my_func1,
    METH_VARARGS, 
    "this is a function named my_func1",
  },
  {NULL, NULL}
};

static PyModuleDef HANSER = {
  PyModuleDef_HEAD_INIT,
  "hanser",
  "this is a module named hanser",
  -1,
  module_functions,
  NULL,
  NULL,
  NULL,
  NULL
};


PyMODINIT_FUNC
PyInit_hanser(void)
{
  return PyModule_Create(&HANSER);
}

这里面涉及很多知识,比如:参数解析、参数类型、定义模块,当然还有如何导入一个模块、调用模块的属性和方法、定义一个类的时候使用类的魔方方法等等等等,我这里就不写了,总之使用Python/C api编写扩展模块是一件非常累的事情。

总结

如果你能够掌握Python/C api的话,那么你一定是一个了不起的人。总之一句话:如果你用C编写扩展模块的时候,能够像写Python一样轻松,或者说Python语言的高级用法,比如魔方方法、描述符、元类、装饰器等等等等,你可以迅速用Python实现的话。那么我可以负责任的告诉你,要是只论Python的话(当然相信你的C水平也是极高的),你可以轻松地进入任何一家公司。

但正因为Python/C api写起来是一件异常痛苦的事情,所以Cython的出现极大的解放了程序猿的双手。

总之扩展类型是Cython将C的性能和Python的外观相结合的一个体现,Cython定义的扩展类型具有如下特性:

  • 允许轻松高效地访问实例的C级数据和方法;
  • 效率高;
  • 允许控制属性的可见性;
  • 可以被Python类继承
  • 等价于内置类型,我们说扩展类不能继承Python类,但是它可以继承内置类型,因为扩展类和内置类是同一级别的。

在后面的系列中我们将更加自由的使用扩展类型,以及使用扩展类型来包装C的结构体、函数、以及C++中的类,从而给外部库提供一个优秀的面向对象的接口。

以上是关于《Cython系列》4. Cython中的扩展类的主要内容,如果未能解决你的问题,请参考以下文章

Cython:扩展类型的链接列表

Cython 中的 C++ 指针

通过pickle保存cython扩展

如何将内置的 Cython 扩展从 PC 转移到另一台?

Cython 将扩展模块传递给 python 导致 to_py_call_code 错误

cython 中的异常处理