第7篇 Cython封裝C++代碼(前)

本篇我們將詳細講解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
ss8.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容