美文网首页IT狗工作室
第7篇 Cython封装C++代码(前)

第7篇 Cython封装C++代码(前)

作者: 铁甲万能狗 | 来源:发表于2020-05-04 19:30 被阅读0次

本篇我们将详细讲解Cython封装C++代码,并如何调用它们,在进行这个主题前,我们需要需要先讲解一下这些概念

  • 定义文件
  • 实现文件
  • cimport 和import语句的区别

Cython还允许我们将项目分解为几个模块。 它完全支持import语句,其含义与Python中的含义相同。这使我们可以在运行时访问在外部纯Python模块中定义的Python对象或在其他扩展模块中定义的Python可访问对象.

Cython文件类型

Cython提供了三种文件类型,可帮助组织项目的Cython特定部分和C级部分。

实现文件(implementation file):到目前为止,我们一直在使用扩展名为.pyx的Cython源文件.

定义文件(Declaration File):其扩展名为.pxd,包含任何C级别可以被其他Cython模块公开访问的如下表项。

  • C类型声明ctypedef、struct、union或enum
  • 外部C或C++库的声明
  • cdef和cpdef模块级函数的声明
  • cdef class 扩展类型的声明
  • 扩展类型的cdef属性
  • cdef和cpdef方法的声明
  • C级内联函数和方法的实现

但定义文件不能包含如下代码

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

包含文件(Include File) ,扩展名为.pxi。

cimport语句

cimport语句能够将.pyx文件、.pxd文件和.pxi文件之间的代码相互关联;使各个Cython源代码构造更大的Cython项目。有了cimport语句和三种文件类型,我们就可以在不影响性能的情况下有效地组织Cython项目

我们通过一个示例来解析一下,比如我们下面有一个关于Fruit扩展类的类定义,以及一些辅助函数的声明,它们位于cy_fruit.pxd中,

#cython:language_level=3
cdef class Fruit(object):
    cdef:
        readonly str name
        public double qty
        readonly double price
        
    cpdef double amount(self)
#end-class

cdef list shop_cart(list itemList ,Fruit item)

cdef double payment(list)

cpdef void display_fruit(Fruit)

在pxd文件中Fruit类定义仅由类属性声明和和类方法的声明,类方法的声明只是包含类方法的签名,并没有类方法的实现代码,这些一切和C++的头文件定义都非常相似,但唯一不同的是Cython并不允许在定义文件中存在类方法的具体实,而在C++中这是允许的

我们有了之前的定义文件,在对应的实现文件中cy_fruit.pyx,我们需要通过cimport语句在实现文件中加载c_fruit.pxd文件中声明类定义和辅助函数声明,即语句from cy_fruit cimport Fruit,shop_cart,payment,并且要实现它们,如果你们有C/C++编程的概念,这是很好理解的。因为我们定义文件是用于编译时实现文件访问它们,Cython提供专用的cimport语句导入.pxd文件或.pyx文件,如下代码所示

#cython:language_level=3
from cy_fruit cimport Fruit,shop_cart,payment

cdef class Fruit(object):
    '''Fruit Type'''
        
    def __cinit__(self,str nm,double qt,double pc):
        self.name=nm
        self.qty=qt
        self.price=pc
        
    cpdef double amount(self):
        return  self.qty*self.price
    
    def __repr__(self):
        return "name:{},qty:{},price:{}".format(
            self.name,self.qty,self.price)
#end-class

cdef list shop_cart(list itemList ,Fruit item):
    
    if item.name!='' and item.qty:
        itemList.append(item)
    return itemList

cdef double payment(list itemList):
    cdef double total=0.0
    if len(itemList[0]):
        for item in itemList[0]:
            total+=item.amount()
        return total

cpdef void display_fruit(Fruit obj):
    print(obj)

因为cimport语句与import语句的语法非常相似,我们还可以这样导入.pxd文件,当我们要实现类中的方法,要加上.pxd文件的名称cy_fruit,跟Python的import一样,我们称cy_fruit这样名称为命名空间,

cimport cy_fruit
....
cdef class cy_fruit.Fruit(object):
      .....
      cpdef double amount(self):
            return self.qty*self.price
#end-class

那么在实现文件中访问定义文件访问Cython扩展类定义,需要这样的格式[命名空间].[类名称],例如:cy_fruit.Fruit

同样,我们也可以导入pxd文件时,cimport语句还可以使用as子句给命名空间设定别名,例如

cimport cy_fruit as cyf
....
cdef class cyf.Fruit(object):
      .....
      cpdef double amount(self):
            return self.qty*self.price
#end-class

同样,我们还可以使用as子句,为导入的具体的类名称,函数名称设定别名

from cy_fruit cimport Fruit as Fru,
      shop_cart as cart,
      payment as pay
....

cimport和import的区别

  • import语句用于运行时导入Python模块(含Cython已编译的扩展模块)/包。尝试导入Cython的cdef关键字声明的数据类型:扩展类,C类型的变量,或函数声明,会产生编译时错误。
  • import语句可以导入cpdef关键声明的函数或类方法,因为cpdef关键字修饰的函数或类方法会在Cython编译器编译扩展模块时,生成该类方法或函数的Python版本包装函数(或类方法的包装函数)
  • cimport语句用于编译时导入Cython定义文件或Cython实现文件,若尝试导入Python级别的对象,变量,函数会产生编译时错误。

一个简单的例子能够说明import和cimport之间的差异,我们看看下面的python脚本app.py

#!/usr/bin/python3

import pyximport
pyximport.install()

from cy_fruit import Fruit
from cy_fruit import display_fruit

if __name__=='__main__':
    f=Fruit("apple",52,33
    display_fruit(f)
    

在app.py中我们通过只能使用import语句导入cy_fruit模块中的Fruit类,同时也能通过import语句导入cpdef关键字声明的函数

  • 在Python上下文中,Python解释器只能识别import语句,无法理解cimport语句。
  • 另外,import语句尝试从已编译的Cython扩展模块中导入cdef关键字声明的函数或变量会提示ImportError错误,因为Python代码是无法访问Cython扩展模块中任何C级别私有属性或cdef声明的函数

cdef extern from语句块

定义文件允许我们使用cdef extern from语句块加载Cython代码以外的纯C/C++代码,并且通过Cython代码进行封装,这样的好处是能够将外部的C/C++的代码能够在Cython源代码中重用

我们对前面的示例进一步扩展,希望按照货币格式打印Fruit对象的价格(price)和销售总金额(amount),这里会用到C++写的MoneyFormator类,该类用于对传入的数字字面量进行货币格式化。

以下是MoneyFormator类接口定义文件,定义在一个叫currency.hh的头文件中

#ifndef MONEYFORMATOR_H
#define MONEYFORMATOR_H
#include <iostream>
#include <iterator>
#include <locale>
#include <string>
#include <sstream>

namespace ynutil{
    class MoneyFormator{
    public:
        MoneyFormator();
        MoneyFormator(const char*);
        ~MoneyFormator();
        
        std::string str(double);
        
    private:
        std::locale loc;
        const std::money_put<char>& mnp;
        std::ostringstream os;
        std::ostreambuf_iterator<char,std::char_traits<char>> iterator;    
    };
}
#endif

MoneyFormator类实现文件,定义在currency.cpp文件中。

#include "currency.hh"

namespace ynutil {
    MoneyFormator::MoneyFormator()
    :loc("zh_CN.UTF-8"),
    mnp(std::use_facet<std::money_put<char>>(loc)),
    iterator(os)
    {
        os.imbue(loc);
        os.setf(std::ios_base::showbase);
    }

    MoneyFormator::MoneyFormator(const char* localName)
    :loc(localName),
    mnp(std::use_facet<std::money_put<char>>(loc)),
    iterator(os)
    {
        os.imbue(loc);
        os.setf(std::ios_base::showbase);
    }
    
    MoneyFormator::~MoneyFormator(){}
        
    std::string MoneyFormator::str(double value){
        //清理之前遗留的字符流
        os.str("");
        mnp.put(iterator,false,os,' ',value*100.0);
        return os.str();
    }
}

Cython封装C++代码

Cython包装C ++类的过程与包装C结构体的过程非常相似

首先,我们需要创建一个定义文件,这里我们命名为currency.pxd,在定义文件中使用cdef external from语句块从currency.hh类定义文件加载MoneyFormator类定义细节。这里还使用namespace关键字为Cython的类定义文件currency.pxd声明了命名空间ynutil,和C++的currency.hh的类定义文件的namespace是一一对应的。

  cdef extern from "currency.hh" namespace "ynutil":

接下来,使用cppclass关键字声明Cython扩展类MoneyFormator,这是告诉Cython编译器正在封装的外部代码是C++代码,并且Cython类的名称和C++版本的MoneyFormator类名称必须一致。完整代码如下

#cython:language_level=3

cdef extern from "currency.cpp":
    pass

from libcpp.string cimport string

cdef extern from "currency.hh" namespace "ynutil":
    cdef cppclass MoneyFormator:
    
        MoneyFormator() except +
        MoneyFormator(const char*) except+
        
        string str(double)

上面示例是一个有效的Cython类声明,有如下细节需要知道的

  • 第一条语句cdef extern from "currency.cpp"这条语句其实就是等价于C++代码中的

    #include "currency.cpp"

    就是告知Cython编译器将MoneyFormator的类实现代码加载到currency.pxd的定义文件中。并且currency.cpp的类定义细节会被pxd文件中的Cython类定义MoneyFormator使用

  • Cython类定义必须嵌套在和C++头文件关联的cdef extern from 语句块中

  • Cython类定义内部声明了允许公开给Python外部代码的类方法。例如默认的构造函数、自定义构造函数、str方法这些声明都是和C++版本的类定义是一一对应的

  • 构造函数的声明追加“ except +”,这是Cython封装C++代码的特殊语法。 如果C ++代码或初始内存分配由于故障而引发异常,这将使Cython可以安全地引发适当的Python异常(请参见下文)。 没有此声明,Cython将不会处理源自构造函数的C ++异常。

上面的Cython封装C++实现的类MoneyFormator,其实就设计三个源代码文件,Cython代码不需要理会C++代码中的细节


在.pxd文件中的Cython类定义中,所谓的封装就是,程序员可以选择性地以相同的类方法名称属性名称以Cython的语法将对应的C++版本的类方法和属性逐个声明一次。本示例中,我们并没有对C++中版本中欧给你的MoneyFormat的私有属性逐个声明一篇,
class MoneyFormator{
    ....
    private:
        std::locale loc;
        const std::money_put<char>& mnp;
        std::ostringstream os;
        std::ostreambuf_iterator<char,std::char_traits<char>> iterator;  
};

因为没必要,首先Cython并不完全支持C++ 标准库中的所有内置扩展数据类型和函数,例如上面C++版本中的std::locale,和std::money_put<T>类模板,这些C++类型在Cython的libcpp目录内预设的C++封装的定义文件中是不存在的类似的locale.pxd的声明,我们可以查看Cython扩展中的include/libcpp目录下,可以得到验证


除非你自行封装对应C++的类型到对应Cython的类声明,以扩展libcpp目录下的数据类型对Cython语法的支援,但默认的libcpp目录下对C++的类型封装已经足够我们编程需要了。

编译扩展模块

# distutils: language=c++
#cython:language_level=3

from currency cimport MoneyFormator
from libcpp.string cimport string

cpdef string money_format(str localName,double n):
    '''重堆中为MoneyFormator类分配内存'''
    cdef MoneyFormator* mon

    try:
        if localName=='' or localName==None:
            mon=new MoneyFormator()
        else:
            mon=new MoneyFormator(localName[0].encode('utf-8'))
        return mon.str(n)
    except Exception as e:
        print(e)

相关文章

网友评论

    本文标题:第7篇 Cython封装C++代码(前)

    本文链接:https://www.haomeiwen.com/subject/dmgyghtx.html