第7篇 Cython封装C++代码(前)
本篇我们将详细讲解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)