zoukankan      html  css  js  c++  java
  • 《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 *,主要是使用char *的话我们只能传递一个ascii字符串。如果传递了非ascii字符串,那么会出现编译错误。我们只能手动编码成bytes,才能传递给char *,但作为中国人这显然不是我们想要的。另外,可能你传递非ascii字符串也能正常通过,但我们之前也说过,这只是你的编译器比较笨,它没有检测出来这是不合法的,然而一旦换了一个聪明一点的编译器,就不会让你通过了,因此我们要写就要写健壮的代码。所以字符串和字节串就使用str和bytes即可,不要使用char *了。

    因此我个人建议,像创建变量、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也是不符合类型的。这里我们设置的是not 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++中的类,从而给外部库提供一个优秀的面向对象的接口。

  • 相关阅读:
    Docker windows下安装,入门及注意事项,并搭建包含Nodejs的webapp
    360浏览器table中的td为空时td边框不显示的解决方法
    关于发布webservice提示The test form is only available for requests from the local machine
    CRM相关SQl手记
    页面右下角弹出的消息提示框
    MS CRM2011 js常用总结
    MVC razor 使用服务器控件
    常用正则表达式
    CRM 2011 常用对象
    人工智能AI-机器视觉CV-数据挖掘DM-机器学习ML-神经网络-[资料集合贴]
  • 原文地址:https://www.cnblogs.com/traditional/p/13277004.html
Copyright © 2011-2022 走看看