• 《Cython系列》5. 组织你的Cython代码


    楔子

    我们之前在介绍Cython语法的时候,一直永远都是一个pyx文件,如果是多个pyx文件改怎么办?怎么像Python那样进行导入呢?

    Python提供了modules和packages来帮助我们组织项目,这允许我们将函数、类、变量等等,按照各自的功能或者实现的业务,分组到各自的逻辑单元中,从而使项目更容易理解和定位。并且模块和包也使得代码重用变得容易,在Python中我们使用import语句访问其它module和package中的函数。

    而Cython也支持我们将项目分成多个模块,首先它完全支持import语句,并且含义与Python中的含义完全相同。这就允许我们在运行时访问外部纯Python模块中定义的Python对象,或者其它扩展模块中定义的可以被访问的Python对象。

    但故事显然没有到此为止,因为只有import的话,Cython是不允许两个模块访问彼此的cdef、cpdef定义的函数、ctypedef、struct等等,也不允许访问对其它的扩展类型进行C一级的访问。

    而为了解决这一问题,Cython提供了三种类型的文件来组织Cython文件一级C文件。到目前为止,我们一直使用扩展名为pyx的Cython源文件,它是包含代码的逻辑的实现文件。但是还有两种类型的文件,分别是扩展名为pxd的文件和扩展名为pxi的文件。

    pxd文件你可以想象成类似于C中的头文件,用于存放一些声明之类的,而Cython的cimport就是从pxd文件中进行属性导入。

    这一节我们就来介绍cimport语句的详细信息,以及pyx、pxd、pxi文件之间的相互联系,我们如何使用它们来构建更大的Cython项目。有了cimport和这三种类型的文件,我们就可以有效地组织Cython项目,而不会影响性能。

    Cython的实现文件(pxd文件)和声明文件(pxi文件)

    我们目前一直在处理pyx文件,它是我们编写具体Cython代码的文件,当然它和py文件是等价的。如果的Cython项目非常小,并且没有其它代码需要访问里面的C级结构,那么一个pyx文件足够了。但如果我们想要共享pyx文件中的C级结构,那么就需要pxd文件了。

    假设我们有这样一个Cython文件:lover.pyx

    # lover.pyx
    from libc.stdlib cimport malloc, free
    
    ctypedef double real
    
    cdef class Girl:
    
        cdef public :
            str name  # 姓名
            long age  # 年龄
            str gender  # 性别
        cdef real *scores  # 分数,这里我们的double数组长度为3,但是real *不能被访问,所以它不可以使用public
    
        def __cinit__(self, *args, **kwargs):
            self.scores = <real *> malloc(3 * sizeof(real))
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
        def __dealloc__(self):
            if self.scores != NULL:
                free(self.scores)
    
        cpdef str get_info(self):
            return f"name: {self.name}, age: {self.age}, gender: {self.gender}"
    	
        cpdef set_score(self, list scores): 
            # 虽然not None也可以写在参数后面,但是它只适用于Python函数
            assert scores is not None and len(scores) == 3
            cdef real score
            cdef long idx
            for idx, score in enumerate(scores):
                self.scores[idx] = score
    
        cpdef list get_score(self):
            cpdef list res = [self.scores[0], self.scores[1], self.scores[2]]
            return res
    

    目前来讲,由于所有内容都在一个pyx文件里面,因此任何C级属性都可以自由访问。

    >>> import cython_test
    >>> cython_test.Girl('古明地觉', 16, 'female')
    <cython_test.Girl object at 0x7f9716ee3308>
    >>> g = cython_test.Girl('古明地觉', 16, 'female')
    >>> 
    >>> g.get_info()
    'name: 古明地觉, age: 16, gender: female'
    >>> g.set_score([90.4, 97.3, 97.6])
    >>> g.get_score()
    [90.4, 97.3, 97.6]
    >>> 
    

    访问非常地自由,没有任何限制,但是随着我们Girl这个类的功能越来越多的话,该怎么办呢?

    所以我们需要创建一个pxd文件,就叫lover.pxd吧,然后把我们希望暴露给外界访问的C级结构放在里面。

    # lover.pxd
    ctypedef double real
    
    cdef class Girl:
    
        cdef public :
            str name  
            long age  
            str gender  
        cdef real *scores  
        
        cpdef str get_info(self)
        cpdef set_score(self, list scores)
        cpdef list get_score(self)
    

    我们看到在pxd文件中,我们只存放了C级结构的声明,像ctypedef、cdef、cpdef等等,并且函数的话我们只是存放了定义,函数体并没有写在里面,同理后面也不可以有冒号。另外,pxd文件是在编译时访问的,而且我们不可以在里面放类似于def这样的纯Python声明,否则会发生编译错误。

    所以pxd文件只放相应的声明,而它们的具体实现是在pyx文件中,因此有人发现了,这个pxd文件不就是C中的头文件吗?答案确实如此。

    然后我们对应的lover.pyx文件也需要修改,lover.pyx和lover.pxd具有相同的基名称,Cython会将它们视为一个命名空间。另外,如果我们在pxd文件中声明了一个函数,那么在pyx文件中不可以再次声明,否则会发生编译错误。怎么理解呢?

    我们说类似于cpdef func(): pass这种形式,它是一个函数(有定义);但是cpdef func()这种形式,它只是一个函数声明。所以函数声明和C中的函数声明也是类似的,在Cython中没有冒号、以及函数体的话,那么就是函数声明。而在Cython的pyx文件中也可以进行函数声明,比如C源文件中也是可以声明函数的,但是一般都会把声明写在h头文件中,在Cython中也是如此,会把C级结构、一些声明写在pxd文件中。

    而一旦声明,那么只能声明一次,不可以重复声明,具有相同基名称的pyx、pxd文件中,函数声明只能出现一次,如果在pxd文件中出现了,那么在pyx中就不可以重复声明了。并且对于函数,如果声明了,那么则需要有具体的实现。

    注意:声明针对的是C一级的结构,所以对于def定义的Python函数是不可以进行声明的。

    重新修改我们的pyx文件

    # lover.pyx
    from libc.stdlib cimport malloc, free
    
    cdef class Girl:
    
        def __cinit__(self, *args, **kwargs):
            self.scores = <real *> malloc(3 * sizeof(real))
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
        def __dealloc__(self):
            if self.scores != NULL:
                free(self.scores)
    
        cpdef str get_info(self):
            return f"name: {self.name}, age: {self.age}, gender: {self.gender}"
    
        cpdef set_score(self, list scores):
            assert scores is not None and len(scores) == 3
            cdef real score
            cdef long idx
            for idx, score in enumerate(scores):
                self.scores[idx] = score
    
        cpdef list get_score(self):
            cpdef list res = [self.scores[0], self.scores[1], self.scores[2]]
            return res
    

    虽然结构没有什么变化,但是我们把一些C级数据拿到pxd文件中了,所以pyx文件中的可以直接删掉了,会自动到对应的pyd文件中找,因为它们有相同的基名称,Cython会将其整体看成一个命名空间。所以:这里pyx文件和pxd文件一定要有相同的基名称,只有这样才能够找得到,否则你会发现代码中real是没有被定义的,当然还有self的一些属性,因为它们必须要使用cdef在类里面进行声明。

    但是哪些东西我们才应该写在pxd文件中呢?本质上讲,任何在C级别上,需要对其它模块公开的,我们才需要写在pxd文件中,比如:

    • C类型声明--ctypedef、结构体、共同体、枚举(后续系列中介绍)
    • 外部的C、C++库的声明(后续系列中介绍)
    • cdef、cpdef模块级函数的声明
    • cdef class扩展类的声明
    • 扩展类的cdef属性
    • 使用cdef、cpdef方法的声明
    • C级内联函数或者方法的实现

    但是,一个pxd文件不可以包含如下内容:

    • Python函数和非内联C级函数、方法的实现
    • Python类的定义
    • IF或者DEF宏的外部Python可执行代码

    那么我们的pxd文件都带来了哪些功能呢?那就是lover.pyx文件可以被其它的pyx文件导入了,这几个pyx文件作为一个整体为Python提供更强大的功能,否则的话其它的pyx文件是无法导入的,导入方式是使用cimport。

    cimport语句

    然后我们在另一个pyx文件中导入这个lover.pyx,当然导入的话其实寻找的是pxd,然后调用的是pyx里面的函数。

    # cython_test.pyx
    from lover cimport Girl
    
    cdef class NewGirl(Girl):
        pass
    

    然后进行编译,不过此时由于涉及到多个pyx文件,因此这些pyx都要进行编译才行。

    from distutils.core import Extension, setup
    from Cython.Build import cythonize
    
    
    ext = [Extension("lover", ["lover.pyx"]),  # 不用管pxd,会自动包含
           Extension("cython_test", ["cython_test.pyx"])]
    
    setup(ext_modules=cythonize(ext, language_level=3))
    

    编译的命令和之前一样,编译之后会发现原来的目录中有两个pyd文件了。

    将这两个文件拷贝出来,首先在cython_test.pyx中,是直接导入的lover,因此这两个pyd文件要在一个目录中。

    >>> from cython_test import NewGirl
    >>> 
    >>> g = NewGirl('古明地觉', 17, 'female')
    >>> g.get_info()
    'name: 古明地觉, age: 17, gender: female'
    >>> 
    >>> g.set_score([90.1, 90.3, 93.5])
    >>> g.get_score()
    [90.1, 90.3, 93.5]
    >>> 
    

    此时两个pyd文件就实现了导入,我们可以将这个cython_test.pyx写的更复杂一些。

    from lover cimport Girl
    
    
    cdef class NewGirl(Girl):
    
        cdef public str where
    
        def __init__(self, name, age, gender, where):
            self.where = where
            super().__init__(name, age, gender)
    
        def new_get_info(self):
            return super(NewGirl, self).get_info() + f", where: {self.where}"
    
    >>> from cython_test import NewGirl
    >>> # 自己定义了__init__,接收4个参数
    >>> g = NewGirl('古明地觉', 17, 'female')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "cython_test.pyx", line 8, in cython_test.NewGirl.__init__
        def __init__(self, name, age, gender, where):
    TypeError: __init__() takes exactly 4 positional arguments (3 given)
    >>> 
    >>> # 传递4个参数,前面3个会交给父类处理
    >>> g = NewGirl('古明地觉', 17, 'female', '东方地灵殿')
    >>> g.get_info()  # 父类的方法
    'name: 古明地觉, age: 17, gender: female'
    >>> 
    >>> g.new_get_info()  # 在父类的方法返回的结果之上,进行添加
    'name: 古明地觉, age: 17, gender: female, where: 东方地灵殿'
    >>> 
    

    因此我们看到使用起来基本上和Python之间是没有区别,主要就是如果涉及到多个pyx,那么这些pyx都要进行编译。并且想被导入,那么该pyx文件一定要有相同基名称的pxd文件。导入的时候使用cimport,会去pxd文件中找,然后具体实现则是去调用pyx文件。但即便pyx文件能被导入,但也未必就能使用其所有的C级结构,想使用的话必须要在对应的pxd文件中进行声明。

    另外,可能有人发现了,我们这是绝对导入,对于目前演示来说使用绝对导入、相对导入是没有什么区别的。但实际上,pyd应该采用相对导入,因为它无法作为启动文件,只能被导入。所以我们在pyx文件中使用相对导入即可。

    我们举个栗子,之前的lover.pyx和lover.pxd不变,在cython_test.pyx进行相对导入。

    from .lover import Girl
    

    但是:上面三个文件在一个单独的目录中,假设就叫lover吧。1.py就是我们执行编译的,它和lover目录是同级的。

    然后编译扩展模块的时候可以用之前的方式编译,只不过Extension中文件路径要指定对。但是这里我们换一种方式吧,我们之前说支持通配符。

    from distutils.core import setup
    from Cython.Build import cythonize
    
    # 将lover目录下的所有pyx文件进行编译,名字和pyx文件名是一致的。
    # 如果是Extension的话,我们可以自己指定
    setup(ext_modules=cythonize("lover/*.pyx", language_level=3))
    

    当编译成功之后,对应的目录就会出现两个扩展模块,我们将其移动过来。

    但是注意:由于涉及到相对导入,所以我们不可以在和cython_test.so同目录下导入,否则会报错。

    >>> import cython_test
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "lover/cython_test.pyx", line 1, in init cython_test
        from .lover import Girl
    ImportError: attempted relative import with no known parent package
    >>> 
    

    我们可以将这两个so文件移动到一个单独的目录中,就移动到lover中吧。

    其实我们还可以在里面写入__init__文件,这样才更符合Python中的包,但这里我们就不写了,只要可以相对导入即可。

    >>> from lover.cython_test import Girl
    >>> 
    >>> g = Girl("古明地觉", 17, "female")
    >>> g.get_info()
    'name: 古明地觉, age: 17, gender: female'
    >>> 
    

    预定义的pxd文件

    还记得我们之前的from libc.stdlib cimport malloc, free这行代码吗?显然这是Cython提供的,没错它就在Cython模块主目录下的Includes目录中,libc这个包下面除了stdlib之外,还有stdio、math、string等pxd文件。除此之外,它还有一个libcpp包对应的pxd文件,里面包含了C++标准模板库(STL)容器,如:string、vector、list、map、pair、set等等。

    而CPython解释器的Include目录中定义了大量的C头文件,而Cython也提供了对应的pxd文件,可以方便地访问Python/C api。当然还有一个最重要的包就是numpy,Cython也是支持的,当然这些我们会在后面系列中介绍了。

    from ... cimport...和from ... import ...的用法是一致的,区别就是前者多了一个c

    下面我们就来演示一下常见的功能。

    from libc cimport math
    print(math.sin(math.pi / 2))
    
    from libc.math cimport cos, pi
    print(cos(pi / 2))
    
    # 使用C++模板
    from libcpp.vector cimport vector
    cdef vector[int] *vi = new vector[int](10)
    
    # 注意:如果使用import和cimport导入了相同的函数,那么会编译错误
    """
    from math import sin
    from lib.math cimport sin
    这种方式是报错的
    解决办法可以通过as
    from math import sin as py_sin
    from lib.math cimport sin as c_sin
    """
    

    我们说pxd文件类似于C的头文件(h文件),它们有以下相似之处:

    • 都通过使用外部代码声明C级结构
    • 都允许我们将一个大文件分成不同的多个组件
    • 都负责声明用于实现的C级接口

    除此之外,还有一个pxi文件,但是个人觉得用处不是很大,这里就不说了。

  • 相关阅读:
    1. shiro-用户认证
    Salt 盐值
    使用ajax向后台发送请求跳转页面无效的原因
    @RequestParam和@RequestBody的区别
    JavaSE:Java11的新特性
    JavaSE: Java10的新特性
    JavaSE:Java9 新特性
    JavaSE:Java8新特性
    JavaSE:Java8新特性
    JavaSE:Java8 新特性
  • 原文地址:https://www.cnblogs.com/traditional/p/13284386.html
Copyright © 2020-2023  润新知