使用C語言編寫Python模塊-引子【轉】


轉自:https://www.jianshu.com/p/47590edc355c

為什么要用C語言寫Python模塊,是Python不夠香么?還是覺得頭發還茂盛?都不是。因為C語言模塊有幾個顯而易見的好處:

  1. 可以使用Python調用C標准庫、系統調用等;
  2. 假設已經有了一堆C代碼實現的功能,可以不用重寫,豈不美滋滋;
  3. 性能?也算;
  4. 其他一些好處。

注:以下代碼基於Python3。

開局舉個栗

In a nutshell,用C編寫Python模塊就是下面幾步:

准備工作
#include<Python.h> // 沒錯,這就夠了,什么stdio.h就都有了 
定義API
static PyObject* say_hello(PyObject* self, PyObject* args) { printf("Hello world, I just a demo."); } 
注冊API
// PyMethodDef 是一個結構體 static PyMethodDef my_methods[] = { { "say", say_hello, 0, "Just show a greeting." }, {NULL, NULL, 0, NULL} }; 
注冊模塊
static struct PyModuleDef my_module = { PyModuleDef_HEAD_INIT, "dummy", NULL, -1, my_methods }; 
初始化
PyMODINIT_FUNC PyInit_mymodule(void) { return PyModule_Create(&my_module); } 
編譯

編譯也可以手動編譯,只不過,懶。。。

from distutils.core import setup, Extension module1 = Extension('dummy', define_macros = [('MAJOR_VERSION', '1'), ('MINOR_VERSION', '0')], sources = ['my_module.c']) setup (name = 'DummyModule', version = '1.0', description = 'This is a demo package', author = 'zmyzhou', author_email = 'no@email.here.com', url = 'https://docs.python.org/extending/building', long_description = '''This is really just a demo package.''', ext_modules = [module1] ) 
運行
export PYTHONPATH=/home/example
(misc) $ python
Python 3.5.2 (default, Oct  8 2019, 13:06:37) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dummy
>>> dummy.say()
Hello world, I just a demo.
>>> 

解剖麻雀啦

總得來說,想用C寫Python擴展模塊步驟基本就是上面提到的這幾個步驟就可以完成(重復啰嗦):

  1. 定義你需要暴露給CPython解析器的函數;
  2. 用一個PyMethodDef結構體列表去給出所有需要暴露的函數的元數據,對第一步中所定義的函數進行映射以及說明,讓解析器知道文怎去構造一個Python調用;
  3. 用一個PyModuleDef去給出此模塊的元數據;
  4. 給出一個當Python解釋器加載該模塊時候的構造函數PyInit_<Module_name>, 其中Module_name表示該模塊的名字,也就是在PyModuleDef中給出的模塊名,例子中是dummy,那么這個函數名最后就是PyInit_dummy

雖然說簡潔是智慧的精華,但是也太簡單了,褲子都脫了,你就給我看這個?
少俠且慢動手,容我解釋解釋。

API 需要符合什么要求?

由於在Python語言中,在幾乎所有場景中對類型時不加以區分的,而C語言是區分類型的,那怎么辦?解決辦法是只用一種C類型表示,而這個類型就是PyObject。而這個PyObject到底是什么可以暫且不管,就好似總說五百年前是一家,究竟五百年前這家戶主是誰,我們很多時候沒必要知道。
此外,由於幾乎多有Python對象對生存在堆上,因此我們接口中的對象(變量)也應該生存在堆上,所以我們用指針來索引,即PyObject*。到此,我們的函數原型呼之欲出。
在Python中我們定義一個函數時這樣子:

def func(*args): # do something here 

那么我們C中定義的函數也類似:

PyObject* func(PyObject* self, PyObject* args) { // I too do something here } 

是不是似曾相識?如果這個函數是個模塊函數,那么self表示NULL或者一個特定指向的指針,如果是類中的方法,self就表示為當前調用該方法的實例;args就表示參數列表。比如,我們覺得上面例子中``say_hello`總是復讀機式輸出同一句話太單調,我們現在想讓他鸚鵡學舌,我們可以改成:

PyObject* echo(PyObject* self, PyObject* args) { const char* what; PyArg_ParseTuple(args, "s", &what); printf("Python said: %s", what); return Py_None; } 

輸出為:


>>> import dummy
>>> dummy.echo('Hello there!')
Python said: Hello there! 
>>> 

上面echo的例子中我們發現了一個奇怪的東西混了進來:PyArg_ParseTuple。這是什么?我說是魔法肯定被打。

輸入參數和返回處理

輸入

上面說過,Python中我們很少關心某個變量是什么類型,我們用PyObject表示所有從Python傳過來的值類型,但是由於C語言是強類型語言,只用一種類型是沒辦法正常工作的。因此我們需要把這種類型變成C語言中相應的類型。就好似古代夜觀天象,每天都可以出現流星,但是一般人也看不懂天象啊,這只能讓星官來解釋,星官根據不同現象來解釋,是大吉大利還是不詳。PyArg_ParseTuple就是做這個翻譯的工作,其函數聲明如下:

int PyArg_ParseTuple(PyObject *args, const char* format, ...); 

其中args就是API中的args參數,format就是你要將args中的對應參數翻譯成C語言中的什么類型。例如上面echo的例子中,我們就將其翻譯成了char*字符串。通過format="s"來指示PyArg_ParseTuple我們傳入的args第一個參數是字符串。如果我們還想多幾個參數,那么怎么辦?好辦。我們使用format="si"來表示我們第一個參數是字符串,第二個參數是整型。

PyObject* echo(PyObject* self, PyObject* args) { const char* what; int count; PyArg_ParseTuple(args, "si", &what, &count); int i = 0; for(; i < count; i++) printf("Python said: %s \n", what); return Py_None; } 

這樣我們的輸出就變成了:

>>> import dummy >>> dummy.echo('repeat my word 3 times.', 3) Python said: repeat my word 3 times. Python said: repeat my word 3 times. Python said: repeat my word 3 times. >>> 

更多關於如何解析Python穿過來的參數的方法以及如何使用相對應的format,請參閱這里

返回

來而不往非禮也。有傳進來的,那就肯定有傳出去的。事情完成沒完成都應該對請求的人有個交代。那我們怎么把特定的C類型變量丟還給Python呢?使用Py_BuildValue,其實就是類似於PyArg_ParseTuple反過來。我們例子中返回來Python中的None,我們也可以返回一句話。例如:

PyObject* echo(PyObject* self, PyObject* args) { const char* what; int count; char* feedback = "Job is done."; PyArg_ParseTuple(args, "si", &what, &count); int i = 0; for(; i < count; i++) printf("Python said: %s \n", what); return Py_BuildValue("s", feedback); } 
>>> fb = dummy.echo('Repeat my word 4 time and give me feedback.', 4) Python said: Repeat my word 4 time and give me feedback. Python said: Repeat my word 4 time and give me feedback. Python said: Repeat my word 4 time and give me feedback. Python said: Repeat my word 4 time and give me feedback. >>> print(fb) Job is done. >>> 

更多細節請參閱這里

怎么注冊API?

注冊API,需要用到一個PyMethodDef結構體,其定義如下:

struct PyMethodDef { const char *ml_name; /* The name of the built-in function/method */ PyCFunction ml_meth; /* The C function that implements it */ int ml_flags; /* Combination of METH_xxx flags, which mostly describe the args expected by the C func */ const char *ml_doc; /* The __doc__ attribute, or NULL */ }; typedef struct PyMethodDef PyMethodDef 

這里主要注意的是ml_flags,它控制着Python怎樣把參數傳過來,我上面例子中用到的一直是METH_VARARGS這也是一種比較常用的標志,它表示我們所注冊的API接收兩個參數,一個self用於表示調用者本身,另一個args表示個tuple。還有其他幾種標志可選。另外注意區分ml_nameml_meth,前者表示在Python中調用時的名字,后者表示在C語言中定義的方法名字。詳情請看這里

怎么注冊模塊?

與注冊API類似,注冊模塊也用到一個結構體PyModuleDef,其定義如下:

typedef struct PyModuleDef{ PyModuleDef_Base m_base; const char* m_name; const char* m_doc; Py_ssize_t m_size; PyMethodDef *m_methods; struct PyModuleDef_Slot* m_slots; traverseproc m_traverse; inquiry m_clear; freefunc m_free; }PyModuleDef; 

怎么看着比我們例子中的多了很多項?其實多出來的我們只需要特別關心m_name, m_doc, m_size, m_methods這四項。第一項PyModuleDef_Base的值肯定是PyModuleDef_HEAD_INIT,這是個宏,具體是啥我們不需要管。
要注意的是,n_name就是將來你在Python中導入該模塊時的名字,比如這里我們設置n_name="dummy",我們在使用的時候就是import dummym_doc就是我們使用dummy.__doc__將輸出的內容,屬於對模塊的說明,例如:

static struct PyModuleDef my_module = { PyModuleDef_HEAD_INIT, "dummy", "Sometimes NO DOC is the best DOC.", -1, my_methods }; 

則輸出為:

>>> import dummy >>> print(dummy.__doc__) Sometimes NO DOC is the best DOC. 

m_methods就是上面注冊的API。詳情看這里

The end? Not yet.

另外還有個很重要的概念就是引用計數,這個一時半會也說不清,這篇文章的目的本來就是拋磚引玉,大概了解用C語言開發Python模塊是個什么流程,我們的目的也達到了。
很繁瑣,我一個寫Python、三行代碼就可以為所欲為的人,怎么忍受得了這些花里胡哨?幸運的是,所有程序員的痛是一樣的,大家都不喜歡繁瑣,大家都追求的是簡潔。因此誕生了Boost.python這種庫,之后由於Boost太龐大,又出現了類似功能的輕量級pybind11。例如使用pybind11,下面代碼個就可以完成我們上面繁瑣的工作:

#include<pybind11/pybind11.h> namespace py = pybind11; char* greet() { return "Hello, World!"; } PYBIND11_MODULE(example, m) { m.doc() = "pybind11 example module"; // Add bindings here m.def("say", greet); } 

然后用一下命令編譯並設置PYTHONPATH:

c++ -O3 -Wall -shared -std=c++11 -I/home/example/playground/pybind11/include my_module.c -o example.so -I/usr/include/python3.5m -I//home/example/playground/pybind11/include -fPIC
export PYTHONPATH=/home/example

Python中執行:

>>> import example >>> example.say() 'Hello, World!' >>> 

瞬間感覺頭發保住了。
等等,不是說用C嗎?為什么最后亂入C++11?都差不多,who cares?

References

https://docs.python.org/3.7/extending/extending.html#the-module-s-method-table-and-initialization-function
https://docs.python.org/3/c-api/index.html
https://www.python.org/dev/peps/pep-0007/
https://github.com/pybind/pybind11

 



 
這就是我的底線!!歡迎搜索關注TensorBoy , 學習使我快樂!


作者:SunnyZhou1024
鏈接:https://www.jianshu.com/p/47590edc355c
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM