python封装c++接口_第13篇: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 #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扩展模块,如下图所示,笔者

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值