本篇我们将详细讲解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 #include #include #include #include 
namespace ynutil{
class MoneyFormator{
public:
MoneyFormator();
MoneyFormator(const char*);
~MoneyFormator();
std::string str(double);
private:
std::locale loc;
const std::money_put& mnp;
std::ostringstream os;
std::ostreambuf_iterator> iterator;
};
}
#endif
MoneyFormator类实现文件,定义在currency.cpp文件中。
#include "currency.hh"
namespace ynutil {
MoneyFormator::MoneyFormator()
:loc("zh_CN.UTF-8"),
mnp(std::use_facet<:money_put>>(loc)),
iterator(os)
{
os.imbue(loc);
os.setf(std::ios_base::showbase);
}
MoneyFormator::MoneyFormator(const char* localName)
:loc(localName),
mnp(std::use_facet<:money_put>>(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& mnp;
std::ostringstream os;
std::ostreambuf_iterator> iterator;
};

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

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

编译扩展模块

我们之前以定义文件的形式对C++代码进行了Cython形式的封装,要将封装后的Cython代码给外部的Python代码调用,我们需要在创建一个实现文件,我们这里将该实现文件命名为money.pyx,Cython允许我们在实现文件中通过不同编程模式给Python代码提供特定Python接口,如下图所示面向过程:通过cpdef函数,而该函数内部调用被封装的C++代码所公开的接口,而外部Python代码调用该cpdef函数的Python版本的包装函数。

面向对象:通过Cython扩展类对C++代码进行调用,而Cython扩展类本身可以给Python代码调用的。

而我们本篇会先介绍面向过程的,下面是一个具体的例子,我们采用了一个cpdef函数,语法上我不想多说什么,语法上的难点前面6篇Cython教程已经说的很清楚

# 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)
finally:
del mon

这里值得一提的是代码中的# distutils: language = c++,会告知Cython编译器将Cython代码先解析为C++代码,进而再编译成可执行模块。因为赋予了C++代码的语义,因此我们能够在Cython代码中使用new操作符为Cython扩展类MoneyFormator,在堆中内存中实例化MoneyFormator,在cdef函数最后,我们需要显式释放内存。

当然,如果你希望在栈上实例化MoneyFormator的话,可以使用在cpdef函数内部声明局部变量,即如下代码

cdef MoneyFormator fmt=MoneyFormator()

编译上面的代码,笔者更喜欢使用cythonize命令,如下所示

cythonize -i -a money.pyx

import我们自己的C扩展模块,笔者