本篇我們將詳細講解Cython封裝C++代碼,并如何調(diào)用它們,在進行這個主題前,我們需要需要先講解一下這些概念
- 定義文件
- 實現(xiàn)文件
- cimport 和import語句的區(qū)別
Cython還允許我們將項目分解為幾個模塊。 它完全支持import語句,其含義與Python中的含義相同。這使我們可以在運行時訪問在外部純Python模塊中定義的Python對象或在其他擴展模塊中定義的Python可訪問對象.
Cython文件類型
Cython提供了三種文件類型,可幫助組織項目的Cython特定部分和C級部分。
實現(xiàn)文件(implementation file):到目前為止,我們一直在使用擴展名為.pyx的Cython源文件.
定義文件(Declaration File):其擴展名為.pxd,包含任何C級別可以被其他Cython模塊公開訪問的如下表項。
- C類型聲明ctypedef、struct、union或enum
- 外部C或C++庫的聲明
- cdef和cpdef模塊級函數(shù)的聲明
- cdef class 擴展類型的聲明
- 擴展類型的cdef屬性
- cdef和cpdef方法的聲明
- C級內(nèi)聯(lián)函數(shù)和方法的實現(xiàn)
但定義文件不能包含如下代碼
- Python或非內(nèi)聯(lián)C函數(shù)或方法的實現(xiàn)
- Python類定義
- IF或DEF宏之外的可執(zhí)行Python代碼
包含文件(Include File) ,擴展名為.pxi。
cimport語句
cimport語句能夠?qū)?pyx文件、.pxd文件和.pxi文件之間的代碼相互關(guān)聯(lián);使各個Cython源代碼構(gòu)造更大的Cython項目。有了cimport語句和三種文件類型,我們就可以在不影響性能的情況下有效地組織Cython項目
我們通過一個示例來解析一下,比如我們下面有一個關(guān)于Fruit擴展類的類定義,以及一些輔助函數(shù)的聲明,它們位于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類定義僅由類屬性聲明和和類方法的聲明,類方法的聲明只是包含類方法的簽名,并沒有類方法的實現(xiàn)代碼,這些一切和C++的頭文件定義都非常相似,但唯一不同的是Cython并不允許在定義文件中存在類方法的具體實,而在C++中這是允許的。
我們有了之前的定義文件,在對應的實現(xiàn)文件中cy_fruit.pyx,我們需要通過cimport語句在實現(xiàn)文件中加載c_fruit.pxd文件中聲明類定義和輔助函數(shù)聲明,即語句from cy_fruit cimport Fruit,shop_cart,payment,并且要實現(xiàn)它們,如果你們有C/C++編程的概念,這是很好理解的。因為我們定義文件是用于編譯時實現(xiàn)文件訪問它們,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文件,當我們要實現(xiàn)類中的方法,要加上.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
那么在實現(xiàn)文件中訪問定義文件訪問Cython擴展類定義,需要這樣的格式[命名空間].[類名稱],例如:cy_fruit.Fruit
同樣,我們也可以導入pxd文件時,cimport語句還可以使用as子句給命名空間設(shè)定別名,例如
cimport cy_fruit as cyf
....
cdef class cyf.Fruit(object):
.....
cpdef double amount(self):
return self.qty*self.price
#end-class
同樣,我們還可以使用as子句,為導入的具體的類名稱,函數(shù)名稱設(shè)定別名
from cy_fruit cimport Fruit as Fru,
shop_cart as cart,
payment as pay
....
cimport和import的區(qū)別
- import語句用于運行時導入Python模塊(含Cython已編譯的擴展模塊)/包。嘗試導入Cython的cdef關(guān)鍵字聲明的數(shù)據(jù)類型:擴展類,C類型的變量,或函數(shù)聲明,會產(chǎn)生編譯時錯誤。
- import語句可以導入cpdef關(guān)鍵聲明的函數(shù)或類方法,因為cpdef關(guān)鍵字修飾的函數(shù)或類方法會在Cython編譯器編譯擴展模塊時,生成該類方法或函數(shù)的Python版本包裝函數(shù)(或類方法的包裝函數(shù))
- cimport語句用于編譯時導入Cython定義文件或Cython實現(xiàn)文件,若嘗試導入Python級別的對象,變量,函數(shù)會產(chǎn)生編譯時錯誤。
一個簡單的例子能夠說明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關(guān)鍵字聲明的函數(shù)。
- 在Python上下文中,Python解釋器只能識別import語句,無法理解cimport語句。
- 另外,import語句嘗試從已編譯的Cython擴展模塊中導入cdef關(guān)鍵字聲明的函數(shù)或變量會提示ImportError錯誤,因為Python代碼是無法訪問Cython擴展模塊中任何C級別私有屬性或cdef聲明的函數(shù)
cdef extern from語句塊
定義文件允許我們使用cdef extern from語句塊加載Cython代碼以外的純C/C++代碼,并且通過Cython代碼進行封裝,這樣的好處是能夠?qū)⑼獠康腃/C++的代碼能夠在Cython源代碼中重用
我們對前面的示例進一步擴展,希望按照貨幣格式打印Fruit對象的價格(price)和銷售總金額(amount),這里會用到C++寫的MoneyFormator類,該類用于對傳入的數(shù)字字面量進行貨幣格式化。
以下是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類實現(xiàn)文件,定義在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結(jié)構(gòu)體的過程非常相似
首先,我們需要創(chuàng)建一個定義文件,這里我們命名為currency.pxd,在定義文件中使用cdef external from語句塊從currency.hh類定義文件加載MoneyFormator類定義細節(jié)。這里還使用namespace關(guān)鍵字為Cython的類定義文件currency.pxd聲明了命名空間ynutil,和C++的currency.hh的類定義文件的namespace是一一對應的。
cdef extern from "currency.hh" namespace "ynutil":
接下來,使用cppclass關(guān)鍵字聲明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類聲明,有如下細節(jié)需要知道的
-
第一條語句cdef extern from "currency.cpp"這條語句其實就是等價于C++代碼中的
#include "currency.cpp"
就是告知Cython編譯器將MoneyFormator的類實現(xiàn)代碼加載到currency.pxd的定義文件中。并且currency.cpp的類定義細節(jié)會被pxd文件中的Cython類定義MoneyFormator使用
Cython類定義必須嵌套在和C++頭文件關(guān)聯(lián)的cdef extern from 語句塊中
Cython類定義內(nèi)部聲明了允許公開給Python外部代碼的類方法。例如默認的構(gòu)造函數(shù)、自定義構(gòu)造函數(shù)、str方法這些聲明都是和C++版本的類定義是一一對應的
構(gòu)造函數(shù)的聲明追加“ except +”,這是Cython封裝C++代碼的特殊語法。 如果C ++代碼或初始內(nèi)存分配由于故障而引發(fā)異常,這將使Cython可以安全地引發(fā)適當?shù)腜ython異常(請參見下文)。 沒有此聲明,Cython將不會處理源自構(gòu)造函數(shù)的C ++異常。
上面的Cython封裝C++實現(xiàn)的類MoneyFormator,其實就設(shè)計三個源代碼文件,Cython代碼不需要理會C++代碼中的細節(jié)

在.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++ 標準庫中的所有內(nèi)置擴展數(shù)據(jù)類型和函數(shù),例如上面C++版本中的std::locale,和std::money_put<T>類模板,這些C++類型在Cython的libcpp目錄內(nèi)預設(shè)的C++封裝的定義文件中是不存在的類似的locale.pxd的聲明,我們可以查看Cython擴展中的include/libcpp目錄下,可以得到驗證

除非你自行封裝對應C++的類型到對應Cython的類聲明,以擴展libcpp目錄下的數(shù)據(jù)類型對Cython語法的支援,但默認的libcpp目錄下對C++的類型封裝已經(jīng)足夠我們編程需要了。
編譯擴展模塊
我們之前以定義文件的形式對C++代碼進行了Cython形式的封裝,要將封裝后的Cython代碼給外部的Python代碼調(diào)用,我們需要在創(chuàng)建一個實現(xiàn)文件,我們這里將該實現(xiàn)文件命名為money.pyx,Cython允許我們在實現(xiàn)文件中通過不同編程模式給Python代碼提供特定Python接口,如下圖所示

面向過程:通過cpdef函數(shù),而該函數(shù)內(nèi)部調(diào)用被封裝的C++代碼所公開的接口,而外部Python代碼調(diào)用該cpdef函數(shù)的Python版本的包裝函數(shù)。
面向?qū)ο?通過Cython擴展類對C++代碼進行調(diào)用,而Cython擴展類本身可以給Python代碼調(diào)用的。
而我們本篇會先介紹面向過程的,下面是一個具體的例子,我們采用了一個cpdef函數(shù),語法上我不想多說什么,語法上的難點前面6篇Cython教程已經(jīng)說的很清楚
# 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類分配內(nèi)存'''
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++代碼,進而再編譯成可執(zhí)行模塊。因為賦予了C++代碼的語義,因此我們能夠在Cython代碼中使用new操作符為Cython擴展類MoneyFormator,在堆中內(nèi)存中實例化MoneyFormator,在cdef函數(shù)最后,我們需要顯式釋放內(nèi)存。
當然,如果你希望在棧上實例化MoneyFormator的話,可以使用在cpdef函數(shù)內(nèi)部聲明局部變量,即如下代碼
cdef MoneyFormator fmt=MoneyFormator()
編譯上面的代碼,筆者更喜歡使用cythonize命令,如下所示
cythonize -i -a money.pyx
