轉自:https://www.jianshu.com/p/47590edc355c
爲什麼要用C語言寫Python模塊,是Python不夠香麼?還是覺得頭髮還茂盛?都不是。因爲C語言模塊有幾個顯而易見的好處:
- 可以使用Python調用C標準庫、系統調用等;
- 假設已經有了一堆C代碼實現的功能,可以不用重寫,豈不美滋滋;
- 性能?也算;
- 其他一些好處。
注:以下代碼基於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 = '[email protected]',
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擴展模塊步驟基本就是上面提到的這幾個步驟就可以完成(重複囉嗦):
- 定義你需要暴露給CPython解析器的函數;
- 用一個
PyMethodDef
結構體列表去給出所有需要暴露的函數的元數據,對第一步中所定義的函數進行映射以及說明,讓解析器知道文怎去構造一個Python調用; - 用一個
PyModuleDef
去給出此模塊的元數據; - 給出一個當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_name
和ml_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 dummy
;m_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
作者:SunnyZhou1024
鏈接:https://www.jianshu.com/p/47590edc355c
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。