Python遇到性能瓶頸怎麼辦?

2021-02-21 猿人學Python

在Python遇到性能瓶頸時怎麼辦?

答案是找對應功能的C/C++程序,把它編譯成CPython模塊,供Python調用來提高性能。

比如Python中做科學計算,數據處理的Numpy模塊就是使用C語言編寫的,Numpy處理速度比Pandas快數倍。Numpy的處理速度一點都不比go語言差。

本文就是介紹如何把C/C++程序編譯成Python模塊。本文偏技術,需要耐著性質看。

Python 作為一個膠水語言,可以很方便的通過C/C++來進行擴展,提高性能。前面我寫了一篇文章介紹如何通過Python的ctypes加載普通的.so庫:

《Python的C/C++擴展之Python直接調用科大訊飛語音識別動態庫》

其實,這還不算真正的用C/C++寫Python的擴展模塊。

本文將介紹如何使用C語言和C++寫Python模塊。

一、Python的C語言接口


Python語言最初是用C語言實現的一種腳本語言,後來被稱為CPython,是因為後來又有其它語言實現的Python,比如Python實現的Python——PyPy,Java語言實現的Python——Jython,.Net實現的Python——IronPython。

CPython具有優良的開放性和可擴展性,並提供了方便靈活的應用程式接口(API),從而使得C/C++程式設計師能夠對Python解釋器的功能進行擴展。

Python的C語言接口很適合封裝C語言實現的各種函數,如果要封裝C++的類,使用boost_python或者SWIG更方便和合適,還有一個類似boost_python的支持C++11的pybind11

1 模塊封裝

假設我們有一個C函數:

/* 文件名:mylib.c */
int addone(int a) {
   return a+1;
}

如果想在Python解釋器中調用該函數,則應該首先將其實現為Python中的一個模塊,這需要編寫相應的封裝接口,如下所示:


/* wrap_mylib.c */
#include <Python.h>
#include "mylib.h"
PyObject* wrap_addone(PyObject* self, PyObject* args)
{
 int n, result;

 if (! PyArg_ParseTuple(args, "i:fact", &n))
   return NULL;
 result = addone(n); /*這裡調用C函數 */
 return Py_BuildValue("i", result);
}
static PyMethodDef mylibMethods[] =
{
 {"addone", wrap_addone, METH_VARARGS, "Add one to N"},
 {NULL, NULL}
};
void initmylib()
{
 PyObject* m;
 m = Py_InitModule("mylib", mylibMethods);
}

上面就是一個典型的Python擴展模塊,它至少應該包含三個部分:導出函數、方法列表和初始化函數。

2 導出函數

要在Python解釋器中調用C語言中的某個函數,首先要為它編寫對應的導出函數,上述例子中的導出函數為wrap_addone。在Python的C語言擴展中,所有的導出函數都具有相同的函數原型:

PyObject* wrap_method(PyObject* self, PyObject* args);

這個函數是Python解釋器和C函數進行交互的接口,一般以wrap_開頭後面跟上C語言的函數名,這樣命名把導出函數和C語言函數對應起來使得代碼更加清晰。它帶有兩個參數:self和args。

參數self 只在C函數被實現為內聯方法(built-in method)時才被用到,通常該參數的值為空(NULL)。
參數args 中包含了Python解釋器要傳遞給C函數的所有參數,通常使用Python的C語言擴展接口提供的函數PyArg_ParseTuple()來獲得這些參數值。

所有的導出函數都返回一個PyObject指針,如果對應的C函數沒有真正的返回值(即返回值類型為void),則應返回一個全局的None對象(Py_None),並將其引用計數增1,如下所示:

PyObject* wrap_method(PyObject *self, PyObject *args)
{
 Py_INCREF(Py_None);
 return Py_None;
}

3 方法列表

方法列表中列出了所有可以被Python解釋器使用的方法,上述例子對應的方法列表為:

static PyMethodDef mylibMethods[] =
{
 {"addone", wrap_addone, METH_VARARGS, "Add one to N"},
 {NULL, NULL}
};

方法列表中的每項由四個部分組成:

方法名是從Python解釋器中調用該方法時所使用的名字。
參數傳遞方式則規定了Python向C函數傳遞參數的具體形式,可選的兩種方式是METH_VARARGS和METH_KEYWORDS,其中METH_VARARGS是參數傳遞的標準形式,它通過Python的元組在Python解釋器和C函數之間傳遞參數,若採用METH_KEYWORD方式,則Python解釋器和C函數之間將通過Python的字典類型在兩者之間進行參數傳遞。

4 初始化函數

所有的Python擴展模塊都必須要有一個初始化函數,以便Python解釋器能夠對模塊進行正確的初始化。Python解釋器規定所有的初始化函數的函數名都必須以init開頭,並加上模塊的名字。對於模塊mylib來說,則相應的初始化函數為:

void initmylib()
{
 PyObject* m;
 m = Py_InitModule("mylib", mylibMethods);
}

當Python解釋器需要導入該模塊時,將根據該模塊的名稱查找相應的初始化函數,一旦找到則調用該函數進行相應的初始化工作,初始化函數則通過調用Python的C語言擴展接口所提供的函數Py_InitModule(),來向Python解釋器註冊該模塊中所有可以用到的方法。

5 編譯連結

要在Python解釋器中使用C語言編寫的擴展模塊,必須將其編譯成動態連結庫的形式。下面以Linux為例,介紹如何將C編寫的Python擴展模塊編譯成動態連結庫:

$ gcc -fpic -shared -o mylib.so \
            -I/usr/include/python2.7 \
           mylib.c wrap_mylib.c

6 在Python中調用

上面編譯生成的Python擴展模塊的動態連結庫,可以在Python中直接import。如下所示:

veelion@gtx:~$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> example.addone(7)
8
>>>

這裡生成的.so動態庫和上一篇中不用Python的C語言生成的動態庫是不一樣的,從生成過程和使用方法就可以看出來,這裡的動態庫使用起來感覺就是一個Python模塊,直接import就可以了。

二、用boost_python庫封裝C++類

安裝boost python庫:

sudo aptitude install libboost-python-dev

示例

下面代碼簡單實現了一個普通函數maxab()和一個Student類:

#include <iostream>
#include <string>

int maxab(int a, int b) { return a>b?a:b; }

class Student {
   private:
       int age;
       std::string name;

   public:
       Student() {}
       Student(std::string const& _name, int _age) { name=_name; age=_age; }

       static void myrole() { std::cout << "I'm a student!" << std::endl; }

       void whoami() { std::cout << "I am " << name << std::endl; }

       bool operator==(Student const& s) const { return age == s.age; }
       bool operator!=(Student const& s) const { return age != s.age; }

};

使用boost.python庫封裝也很簡單,如下代碼所示:

#include <Python.h>
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
#include <vector>

#include "student.h"

using namespace boost::python;

BOOST_PYTHON_MODULE(student) {                                                                                                                                                                              
   // This will enable user-defined docstrings and python signatures,
   // while disabling the C++ signatures
   scope().attr("__version__") = "1.0.0";
   scope().attr("__doc__") = "a demo module to use boost_python.";
   docstring_options local_docstring_options(true, false, false);
   def(
           "maxab", &maxab, "return max of two numbers.\n"
      );  

   class_<Student>("Student", "a class of student")
       .def(init<>())
       .def(init<std::string, int>())
       // methods for Chinese word segmentation
       .def(
               "whoami", &Student::whoami, "method's doc string..."
           )  
       .def(
               "myrole", &Student::myrole, "method's doc string..."
           )  
       .staticmethod("myrole");
   // 封裝STL
   class_<std::vector<Student> >("StudentVec")
       .def(vector_indexing_suite<std::vector<Student> >())
       ;  
}

上述代碼還是include了Python.h文件,如果不include的話,會報錯誤:

wrap_python.hpp:50:23: fatal error: pyconfig.h: No such file or directory

編譯

編譯以上代碼有兩種方式,一種是在命令行下面直接使用g++編譯:

g++ -I/usr/include/python2.7  -fPIC wrap_student.cpp -lboost_python -shared -o student.so

首先指定Python.h的路徑,如果是Python 3的話就要修改為相應的路徑,編譯wrap_student.cpp要指定-fPIC參數,連結(-lboost_python)生成動態庫(-shared)。生成的student.so動態庫就可以被python直接import使用了

In [1]: import student

In [2]: student.maxab(2, 5)
Out[2]: 5

In [3]: s = student.Student('Tom', 12)

In [4]: s.whoami()
I am Tom

In [5]: s.myrole()
I'm a student!

另外一直方法是用python的setuptools編寫setup.py腳本:

#!/usr/bin/env python

from setuptools import setup, Extension

setup(name="student",
   ext_modules=[
   Extension("student", ["wrap_student.cpp"],                                                                                                                                                              
   libraries = ["boost_python"])
])

然後執行命令編譯:

python setup.py build
or
sudo python setup.py install



三、SWIG封裝C++類

Python調用C/C++代碼的利器除了boost_python外,還有SWIG(Simplified Wrapper and Interface Generator),它是用來為腳本語言調用C和C++程序的軟體開發工具,它實際上是一個編譯器,獲取C/C++的聲明和定義,用一個殼封裝起來,以便其它腳本語言訪問這些聲明。所以,SWIG 最大的好處就是將腳本語言的開發效率和 C/C++ 的運行效率有機的結合起來。

一個雙數組Trie Tree的實現:cedar在中文分詞、新詞發現等算法中可以y用於詞典的創建。本文以cedar的SWIG封裝實現來說明SWIG的使用。

0. 安裝swig

工欲善其事必先利其器,首先要安裝swig,Ubuntu安裝swig很簡單:

sudo aptitude install swig

1. 聲明和定義C/C++代碼

在cedar的swig目錄下面有cedar的C++聲明和實現代碼trie.h,但是這個實現裡面沒有遍歷所有key的函數方法,所以我添加了一個實現,首先定義一個數據結構來定義key:

// key-value pair return type for next_key()
class kv_t {
   public:
       std::string key;
       int value;
};

添加一個函數每次返回一個key,當key字符串為空時表示遍歷結束,繼續調用的話就又從頭開始遍歷:

 // to iterate all keys
 kv_t next_key() const {
   static size_t from = 0, p = 0;
   union { int i; int x; } b;
   char key[256] = {0};
   kv_t kv;
   if(from == 0) {
       b.i = _t->begin(from, p);
   }else{
       b.i = _t->next(from, p);
   }
  if (b.i == trie_t::CEDAR_NO_PATH) {
       kv.key = "";
       kv.value = 0;
       from = 0;
       p = 0;
       return kv;
   }
   _t->suffix(key, p, from);
   kv.key = key;
   kv.value = b.x;
   return kv;
 }

2. 編寫接口文件.i

查看cedar.i可以看到SWIG的接口文件的編寫規則:

首先在 %module  後面聲明模塊名稱,這就是Python在import時使用的模塊名稱;

在%{ … %}之間包含相關頭文件

在%include 後面可以聲明對STL的支持

最後聲明要封裝的函數和變量,也可以之間包含頭文件:%include 「trie.h」

3. 封裝代碼

可以在Makefile裡面看到python-bindings:

python-bindings:
       swig -Wall -python -builtin -outdir python -c++ cedar.i
       mv -f cedar_wrap.cxx python

直接make或者單獨運行上面的swig命令,就可以生成cedar.py和cedar_wrap.cxx文件。

4. 編譯生成動態庫

編譯生成的cedar_wrap.cxx使用python distutils的setup,可以參考python/setup.py的編寫。setup.py的build如下:

python setup.py build

就會在當前目錄下面創建目錄build,下面生成lib.linux-x86_64-2.7/cedar.py 和 _cedar.so

四、 pybind11封裝C++

從pybind11的名字可以看出,它是用來封裝C++ 11代碼為Python模塊的庫。它的目標和用法都是想Boost_python庫看齊,但是它又比龐大的Boost庫精簡。我知道這個庫的時間不長,也沒有具體實踐過。以前都是寫C++,然後用boost封裝。但是,感覺pybind11更簡潔,所以下一個項目可以試試它。到時候再分享使用心得給大家。

一個十年Python碼奴與運營汪的結合體

長按掃碼關注

相關焦點

  • Python 性能優化
    我們也知道80/20定律,絕大多數的時間都耗費在少量的代碼片段裡面了,優化的關鍵在於找出這些瓶頸代碼。方式很多:到處加log列印時間戳、或者將懷疑的函數使用timeit進行單獨測試,但最有效的是使用profile工具。python profilers對於python程序,比較出名的profile工具有三個:profile、cprofile和hotshot。
  • 速度or實用性:Python真的遇到瓶頸了嗎?
    你若查看為解決特定問題而優化的許多Python庫的性能基準,就會發現它們與編譯語言相比,表現相當不錯。例如,看看FastAPI的性能基準——顯然,Go作為一種編譯語言要比Python快得多。儘管如此,FastAPI在構建REST API方面還是勝過了一些Go的庫:網絡框架基準(上面的列表不包括C++和Java web框架,它們的性能甚至更高。)
  • Python 性能優化的20條招數
    如果想要在 python 中使用已經有C類庫,使用 ctypes 是很好的選擇,有一些基準測試下,python2+ctypes 是性能最好的方式。Cython: Cython 是 CPython 的超集,用於簡化編寫C擴展的過程。Cython 的優點是語法簡潔,可以很好地兼容 numpy 等包含大量C擴展的庫。
  • Python性能優化的20條招數
    如果想要在 python 中使用已經有C類庫,使用 ctypes 是很好的選擇,有一些基準測試下,python2+ctypes 是性能***的方式。Cython: Cython 是 CPython 的超集,用於簡化編寫C擴展的過程。Cython 的優點是語法簡潔,可以很好地兼容 numpy 等包含大量C擴展的庫。Cython 的使得場景一般是針對項目中某個算法或過程的優化。
  • 為Python正名:語言本身不是瓶頸,可能是外部資源出了問題
    並非如此,如果了解一下為解決特定問題而優化的許多 Python 庫的性能基準測試,你就會發現 Python 比編譯型語言做得好。以 FastAPI 性能基準為例,FastAPI 是一個用於構建 API 的現代、快速(高性能)的 web 框架,使用 Python 3.6+ 並基於標準的 Python 類型提示。
  • 考研複習遇到瓶頸,該怎麼辦?
    當考研遇到瓶頸期,是不是也是因為你的某項基本功不紮實造成的呢?不妨靜下心來,反思整個複習過程,尤其是你的錯題本,很可能阻礙你進步的,就是你當初複習求快而忽略的幾個概念、幾條公式。用兩三天的時間,把知識框架重新梳理一遍,存疑之處盡數標記之,重新鞏固。
  • 讀書筆記丨工作三五年,工作遇到發展瓶頸了,怎麼辦?
    工作三五年,工作遇到發展瓶頸了,怎麼辦?最近一直在看黃有璨老師寫的《非線性成長》這本書,這本書說的是一些職業發展道路上的通關思考和方法。當我們在職場生活中遇到發展瓶頸的時候,通常是我們處於S型曲線的那個成熟階段。2.第二曲線:為職業發展注入新的活力什麼是第二曲線?簡單來說,它就是第二條S型曲線,也就是說我們要在第一條S型曲線沒落之前,找到新的一條S型曲線,幫助我們找到新的職業發展突破口。
  • Python 性能分析入門指南
    分析程序的性能可以歸結為回答四個基本問題:正運行的多快速度瓶頸在哪裡內存使用率是多少內存洩露在哪裡下面,我們將用一些神奇的工具深入到這些問題的答案中去。用 time 粗粒度的計算時間讓我們開始通過使用一個快速和粗暴的方法計算我們的代碼:傳統的 unix time 工具。
  • 做圖文自媒體遇到瓶頸怎麼辦?突破瓶頸就是鳳凰泥槃,一次飛躍
    開始寫旅遊方面的圖文,內容的來源自己旅遊的一些經歷,寫了一段時間,經歷枯竭了,怎麼辦?就想著寫系列的東西,主攻旅遊攻略,三山五嶽,知名5A景區,……這麼一條線,供我寫了一段時間。寫旅遊攻略,不像寫自己感受那麼簡單,因為要準備好多資料,怎麼遊?遊幾天?要在攻略中回答這些問題,你就必須多做功課,不然寫出來不符合實際,那就是敗筆,因為別人讀你的文章,就是為了做旅遊線路攻略。
  • Python 調用 C 模塊以及性能分析
    (點擊上方藍字,快速關注我們)來源:Xjngwww.cnblogs.com/Xjng/p/5120853.html如有好文章投稿,請點擊 → 這裡了解詳情一.c,ctypes和python/hello_world.so')c_lib.hello_world()二.測試c的性能和python的差別sum.cint sum(int num){    long sum=0;    int i =0;    for( i=1;i<=num;i++){        sum=sum
  • 介紹幾款Python性能優化工具
    今天我們就來分享一些平時能用上的Python性能分析工具。memory_profilermemory_profiler是監控python進程的神器,只需要在函數加一個裝飾器就可以輸出每行代碼的內存使用情況安裝:pip install memory_profiler使用:import
  • 學習街舞遇到瓶頸怎麼辦,記住這幾點很重要!
    對於不同的類型的舞者有不同的方法去攻克這一瓶頸,但是大同小異,主要是從以下幾個方面來做,慢慢的就會跨過這個階段,讓自己的水平提升一個等級。第四、心態建設,遇到瓶頸很正常,說明快要升級了(就像是鬥皇突破到鬥尊一樣....哈哈),但需要一個過程,在這個過程中你有足夠多的沉澱,厚積薄發,等到了某一時間突然領悟了,你就突破瓶頸期了,這時候你會發現自己的水平提高了,連心態都不一樣了。
  • 減肥遇到瓶頸怎麼辦?不用節食不用吃藥,總結5點經驗告訴你
    多人在減肥的時候用不了多長時間就會進入一個瓶頸,不管怎樣減體重就是不往下掉,所以很多人就會放棄。今天小編分享給大家減肥成功人士總結出的5點經驗全部都告訴你。減肥遇到瓶頸怎麼辦?不用節食不用吃藥,總結5點經驗告訴你。
  • python性能提高10倍的通用方法
    這種工具可用來存儲和處理大型矩陣,比Python自身的嵌套列表要高效的多,支持大量的維度數組與矩陣運算,此外也針對數組運算提供大量的數學函數庫,在整個python生態中佔有舉足輕重的作用,本篇文章旨在講解如何藉助它來提升python的計算性能。
  • 用Python使用C語言程序(Windows平臺)
    qianyan在機器學習中,很多時候我們需要Python和C的混合編程,最重要的原因是為了性能效率的提升: 解釋型語言一般比編譯型語言慢,一般提高性能的有效做法是,先做性能測試,找出性能瓶頸部分,然後把瓶頸部分在擴展中實現。本文的目標是在windows平臺下(使用pycharm),實現python調用C語言編寫的程序。
  • 日語水平一直進步慢或者遇到了瓶頸怎麼辦?
    日語水平一直進步慢或者遇到了瓶頸怎麼但是日本村小編並不是指一下子讓你進到一個難度很誇張的區域,這樣也完全超出了你日語水平,這樣也是不合理的,我們要學會選擇,找到一個合適的位置來。
  • 專科生考研會遇到瓶頸?這些要點你必須知道
    考研對於專科生來說是一次難得提升自我的機會,相信每年專科生考研的人不在少數,同時,很多專科生都會遇到一個難題,心裡反覆問自己是否適合考研是否會遇到很多難題?三大瓶頸考研總會遇到瓶頸,只要你學會克服,一切都會離成功更近一步,當然,我們也需要去了解一下,在考研中會遇到的問題,這些你必須要知道的~~
  • Python性能分析技巧
    1.分析一行代碼要檢查一行python代碼的執行時間,請使用**%timeit**。3.代碼塊中的每一行代碼進行時間分析到目前為止,我們只在分析一行代碼或代碼塊時查看摘要統計信息,如果我們想評估代碼塊中每一行代碼的性能呢?使用Line_profiler 。Line_profiler 包可用於對任何函數執行逐行分析。
  • python匯總數據弄懂這個,除了性能,沒有什麼難得倒你了--按條件處理數據
    我們主要還是使用apply方法來處理處理班級排名後3位的人員我們通過apply來處理的話,就是性能會有點影響,但是這點影響對於我們日常處理來說問題不是很大,因為真的很快。閒話少說,直接上代碼。有什麼不清楚以及執行過程中遇到了問題,歡迎在群裡提問。提問時,記得要貼當時報錯的截圖。特別要注意代碼與報錯信息要一致。提供python交流和答疑群:加入本群要求是關注過本公眾號的同學,如果發現未關注,則會被定期清理,請知悉。
  • Python 為了提升性能,竟運用了共享經濟
    現在各行各業可是都掀起了一股python學習的熱潮,幾乎每位職場人士都在學習和使用python。python大家都很熟悉了,共享經濟大家也都知道,可是python運用了共享經濟這件事你了解嗎?想知道的話,就來看小編今天跟大家分享的這篇「Python 為了提升性能,竟運用了共享經濟」的文章吧。