使用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 = '[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擴展模塊步驟基本就是上面提到的這幾個步驟就可以完成(重複囉嗦):

  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
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章