[轉] C/C++ 調用Python

from :  https://cyendra.github.io/2018/07/10/pythoncpp/

目錄

  • 前言
  • 官方文檔
  • 環境搭建
  • 編譯連接
  • Demo
  • 解釋器
  • 初始化
  • GIL
  • Object
  • 一切皆對象
  • 從Python代碼中獲取Object
  • C/C++與Object轉換
  • 函數調用
  • 引用計數
  • 參考資料

前言

最近項目中遇到須要用C++調用python代碼的狀況,在網上搜索後發現中文資料比較少。所以藉此機會一邊學習一邊整理成文檔,方便後續查閱。html

官方文檔

教程:https://docs.python.org/2/extending/embedding.html API:https://docs.python.org/2/c-api/index.htmlpython

環境搭建

編譯連接

使用python提供的C/C++接口,須要包含python安裝目錄下的頭文件Python.h 編譯、連接時須要指定頭文件、python庫的地址,不過不須要咱們本身操心,python提供了一個腳本,能夠自動推薦編譯、連接參數: Bash python-config –cflags python-config –ldflagsgit

Demo

動過運行一個簡單的demo,能夠驗證鏈路是否打通。 C++github

#include <Python.h>
int main(int argc, char *argv[]) {
  Py_Initialize();
  PyRun_SimpleString("print('hello world')\n");
  Py_Finalize();
  return 0;
}
// g++ main.cpp -I$PYTHON_PATH/include/python2.7 -lpython2.7
// 輸出 hello world

解釋器

初始化

在調用python API時,首先須要初始化全局解釋器,而且在使用完後銷燬。在咱們的業務場景下,須要解釋器常駐內存,所以Py_Initialize在系統初始化時調用,Py_Finalize在析構函數中調用。api

C++安全

void Py_Initialize(void)
int Py_IsInitialized(void)
void Py_Finalize()

初始化Python後,能夠經過int PyRun_SimpleString(const char *command)函數令解釋器執行任意python代碼。這種叫作高層接口。高層接口雖然方便,但很難與C/C++交換數據。因此對於複雜需求,應該使用低層接口。雖然須要多寫不少C代碼,但能夠靈活的實現不少複雜功能。數據結構

從操做步驟上看,C++調用Python低層接口能夠分爲幾個階段多線程

  • 初始化Python解釋器
  • 從C++到Python轉換數據
  • 用轉換後的數據作參數調用Python函數
  • 把函數返回值轉換爲C++數據結構

GIL

在使用python解釋器時,要注意GIL(全局解釋鎖)的工做原理以及對性能的影響。GIL保證在任意時刻只有一個線程在解釋器中運行。在多線程環境中,python解釋器工做原理以下: Plain Textapp

1. 設置GIL
2. 切換到一個線程去運行
3. 運行:
    a. 指定數量的字節碼指令,或者
    b. 線程主動讓出控制(能夠調用time.sleep(0))
4. 把線程設置爲睡眠狀態
5. 解鎖GIL
6. 再次重複以上全部步驟

對性能的影響: 假若有一段兩個線程的python代碼,運行在一個兩核CPU上。因爲GIL的存在,兩個線程沒法真正並行執行,CPU佔用率老是低於50%。python2.7

GIL是一個歷史遺留問題,致使CPython多線程不能利用多個CPU內核的計算能力。爲了利用多核,一般使用多進程的方法,或是經過Python調用C代碼,由C來實現多線程。

注意,當在C/C++建立的線程中調用Python時,GIL須要經過函數PyGILState_Ensure()和PyGILState_Release()手動獲取、釋放。 C++

PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

/* Perform Python actions here. */
result = CallSomeFunction();
/* evaluate result or handle exception */

/* Release the thread. No Python API allowed beyond this point. */
PyGILState_Release(gstate);

Object

###一切皆對象 在python中有一句話叫作「一切皆對象」,這句話能夠結合源碼更好的進行理解。在python裏,一切變量、函數、類等,在解釋器中執行時,都會在在堆中新建一個對象,並將名字綁定在對象上。 Python

i = 1              -----新建一個PyIntObject對象,而後綁定到i上
s = "abcde"        -----新建一個PyStringObject對象,綁定到s上
def foo(): pass    -----新建一個PyFunctionObject對象, 綁定到foo上
class C(object): pass    -----新建一個類對象,綁定到C上
instance = C()           -----新建一個實例對象,綁定到instance上
l = [1,2]                -----新建一個PyListObject對象,綁定到l上
t = (1,2)                -----新建一個PyTupleObject對象,綁定到t上

在Python/C API中,使用指向堆中對象的指針PyObject*對這些對象進行進行管理。所以,python中的大多數語句,均可以經過對PyObject指針調用各類函數來實現。

從Python代碼中獲取Object

如上一節所述,既然一切皆對象,那咱們就能夠在C/C++中獲取到python代碼中的對象。

C++

// Python內建函數import,導入一個Python模塊。
PyObject* PyImport_ImportModule(char *name) 

// Python語句o.attr_name,返回對象o中檢索attr_name屬性或方法。
PyObject* PyObject_GetAttrString(PyObject *o, char*attr_name)

C/C++與Object轉換

能夠經過調用Py_BuildValue,經過傳遞格式字符串和變長參數,將C/C++變量構造爲變量或元組。 C

PyObject* Py_BuildValue(const char *format, ...)
// 更多參數查閱參考官方文檔
// s 將C字符串轉換爲Python字符串對象。
// i 將C int轉換爲Python整數對象。
// d 將C double轉換爲Python浮點數。

此外也能夠直接調用下面一系列函數,顯式將C/C++變量轉換爲python變量。 C++

// 基本變量
PyObject* PyLong_FromLong(long v)
PyObject* PyBool_FromLong(long v)
PyObject* PyFloat_FromDouble(double v)

// python元組
PyObject* PyTuple_New(Py_ssize_t len)
int PyTuple_SetItem(PyObject *p, Py_ssize_t pos, PyObject *o)

使用例 C++

// 經過set item的方式構造tuple
PyObject* args = PyTuple_New(3);
PyObject* arg1 = Py_BuildValue("i", 100); // 整數參數
PyObject* arg2 = Py_BuildValue("f", 3.14); // 浮點數參數
PyObject* arg3 = Py_BuildValue("s", "hello"); // 字符串參數
PyTuple_SetItem(args, 0, arg1);
PyTuple_SetItem(args, 1, arg2);
PyTuple_SetItem(args, 2, arg3);

// 經過buildvalue直接構造tuple
PyObject* args = Py_BuildValue("(ifs)", 100, 3.14, "hello");
PyObject* args = Py_BuildValue("()"); // 無參函數

PyObject也能夠轉換爲C++變量 C++

// 使用一系列庫函數轉換基本變量
long PyLong_AsLong(PyObject *obj)
long PyInt_AsLong(PyObject *obj)
double PyFloat_AsDouble(PyObject *obj)
string PyString_AsString(PyObject *obj)

// 元組
Py_ssize_t PyTuple_Size(PyObject *p)
PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos)

函數調用

因爲Python中一切皆對象,所以函數、方法等調用均可以經過PyObject_Call系列函數完成。 C++

// callable(*args, **kwargs)
PyObject* PyObject_Call(PyObject *callable, PyObject *args, PyObject *kwargs)
PyObject* PyObject_CallObject(PyObject *callable, PyObject *args)

// callable(arg1, arg2, ...)
PyObject* PyObject_CallFunction(PyObject *callable, const char *format, ...)
PyObject* PyObject_CallFunctionObjArgs(PyObject *callable, ..., NULL)

// obj.name(arg1, arg2, ...)
PyObject* PyObject_CallMethod(PyObject *obj, const char *name, const char *format, ...)
PyObject* PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ..., NULL)

能夠發現,函數參數一般有兩種類型,一種是直接傳遞tuple、map兩類PyObject,另外一種是經過格式字符串與變長參數,直接將C/C++變量解析成參數。

還用一種結合兩種參數的方法,經過PyArg_ParseTuple()、PyArg_ParseTupleAndKeywords()和PyArg_Parse()三個函數,能夠用格式字符串將C/C++變量構造爲Python元組、字典或變量,以便後續函數調用。

引用計數

內存管理

Python使用引用計數與垃圾回收來管理內存。對於每一個對象,能夠理解爲有一個對象實體以及若干對該實體的引用(指向對象的指針)。引用計數經過記錄對象被引用的次數來管理對象,增長對對象的引用會使引用計數加一,減小對對象的引用使引用計數減一,當一個對象引用計數爲0時釋放該對象佔用的內存。因爲引用計數沒法處理循環引用的狀況,還會有垃圾回收機制來處理循環引用的對象。能夠認爲Python解釋器會週期的調用垃圾回收。 全部的python對象,PyObject都有對象類型和引用計數。對象類型肯定它是什麼樣的對象 (如,整數、列表或用戶定義的函數)。對於每一個已知類型,都有一個宏來檢查對象是否屬於該類型。例如,若是指向的對象是 Python 列表, 則 PyList_Check (a) 爲 true。

Python/C API 中的引用計數

API中使用兩個宏Py_INCREF(x) 和 Py_DECREF(x)來處理引用計數的增長和減小。當計數達到零時,Py_DECREF() 會釋放對象。若是是列表等複合對象類型,Py_DECREF還會遞減對象中包含的其餘對象的引用計數。若是忘記減小引用計數將會形成內存泄漏。 有一個概念叫作引用的全部權,擁有全部權表明該引用不使用時須要調用Py_DECREF。全部權能夠被傳遞,得到全部權的新引用也須要在不使用時調用Py_DECREF。 此外還有一個概念叫作「借用(borrow)」的引用,相似C++中std::weak_ptr。借用的引用不能長期持有對象,沒有全部權。對象原持有者釋放對象後,借用引用再訪問被釋放的內存會有風險。借用的引用能夠經過調用Py_INCREF()改成真正持有引用。 全部權傳遞常發生在函數調用:

  • 做爲返回值:當函數返回一個引用給調用方時,分爲兩種狀況:調用方得到引用全部權或借用了引用而沒有得到全部權。
    • 以 PyObject_、PyNumber_、PySequence_ 或 PyMapping_ 開頭的函數老是傳遞全部權,新增它們返回的對象的引用計數,調用方要在使用完後調用Py_DECREF ()。
    • 而PyTuple_GetItem ()、PyList_GetItem ()、PyDict_GetItem () 和 PyDict_GetItemString () 都返回從元組、列表或字典中借用的引用。
  • 做爲參數:當引用做爲參數傳遞給函數時,有兩種狀況:函數竊取引用或沒有竊取。竊取引用表明調用方不須要再處理該引用。
    • 不多有函數竊取引用,經常使用的是PyList_SetItem()和PyTuple_SetItem()。
    • 而PyObject_SetItem()、PyDict_SetItem()不竊取引用。

用一個例子來講明全部權傳遞問題,分別用PyList_GetItem()、 PySequence_GetItem()實現對list中全部數字求和: C++

long
sum_list(PyObject *list)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;

    n = PyList_Size(list);
    if (n < 0)
        return -1; /* Not a list */
    for (i = 0; i < n; i++) {
        item = PyList_GetItem(list, i); /* Can't fail */
        if (!PyLong_Check(item)) continue; /* Skip non-integers */
        value = PyLong_AsLong(item);
        if (value == -1 && PyErr_Occurred())
            /* Integer too big to fit in a C long, bail out */
            return -1;
        total += value;
    }
    return total;
}
C++
long
sum_sequence(PyObject *sequence)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;
    n = PySequence_Length(sequence);
    if (n < 0)
        return -1; /* Has no length */
    for (i = 0; i < n; i++) {
        item = PySequence_GetItem(sequence, i);
        if (item == NULL)
            return -1; /* Not a sequence, or other failure */
        if (PyLong_Check(item)) {
            value = PyLong_AsLong(item);
            Py_DECREF(item);
            if (value == -1 && PyErr_Occurred())
                /* Integer too big to fit in a C long, bail out */
                return -1;
            total += value;
        }
        else {
            Py_DECREF(item); /* Discard reference ownership */
        }
    }
    return total;
}

參考資料

  • 嵌套python解釋器
  • 淺析 C++ 調用 Python 模塊
  • C/C++與python互相調用
  • python 一切皆對象
  • 深刻理解 GIL:如何寫出高性能及線程安全的 Python 代碼
  • Python高級特性:全局解釋器鎖GIL基本概念
  • python引用計數和gc垃圾回收
相關文章
相關標籤/搜索