1. 程式人生 > >【轉載】關於Python混合程式設計時的記憶體洩露

【轉載】關於Python混合程式設計時的記憶體洩露

 

     登陸論壇  | 論壇註冊| 加入收藏 | 設為首頁| RSS
 
  
首頁Linux頻道軟體下載開發語言嵌入式頻道開源論壇
| php | JSP | ASP | asp.net | JAVA | c/c++/c# | perl | JavaScript | Basic | Delphi | 
 
 
您當前的位置:首頁 > 開發語言 > c/c++/c#
    
使用C/C++擴充套件Python
時間:2007-12-12 12:33:14  來源:Linux聯盟收集整理  作者:
如果你會用C,實現Python嵌入模組很簡單。利用擴充套件模組可做很多Python不方便做的事情,他們可以直接呼叫C庫和系統呼叫。

為了支援擴充套件,Python API定義了一系列函式、巨集和變數,提供了對Python執行時系統的訪問支援。Python的C API由C原始碼組成,幷包含 “Python.h” 標頭檔案。

編寫擴充套件模組與你的系統相關,下面會詳解。1   一個簡單的例子
下面的例子建立一個叫做 “spam” 的擴充套件模組,呼叫C庫函式 system() 。這個函式輸入一個NULL結尾的字串並返回整數,可供Python呼叫方式如下:

>>> import spam>>> status=spam.system("ls -l")
一個C擴充套件模組的檔名可以直接是 模組名.c 或者是 模組名module.c 。第一行應該匯入標頭檔案:

#include <Python.h>
這會匯入Python API。

Warning

因為Python含有一些預處理定義,所以你必須在所有非標準標頭檔案匯入之前匯入Python.h 。

Python.h中所有使用者可見的符號都有 Py 或 PY 的字首,除非定義在標準標頭檔案中。為了方便 “Python.h” 也包含了一些常用的標準標頭檔案,包括<stdio.h>,<string.h>,<errno.h>,< stdlib.h>。如果你的系統沒有後面的標頭檔案,則會直接定義函式 malloc() 、 free() 和 realloc() 。

下面新增C程式碼到擴充套件模組,當呼叫 “spam.system(string)” 時會做出響應:

static PyObject*spam_system(PyObject* self, PyObject* args) {    const char* command;    int sts;    if (!PyArg_ParseTuple(args,"s",&command))        return NULL;    sts=system(command);    return Py_BuildValue("i",sts);}
呼叫方的Python只有一個命令引數字串傳遞到C函式。C函式總是有兩個引數,按照慣例分別叫做 self 和 args 。

self 引數僅用於用C實現內建方法而不是函式。本例中, self 總是為NULL,因為我們定義的是個函式,不是方法。這一切都是相同的,所以直譯器也就不需要刻意區分兩種不同的C函式。

args 引數是一個指向Python的tuple物件的指標,包含引數。每個tuple子項對應一個呼叫引數。這些引數也全都是Python物件,所以需要先轉換成C值。函式 PyArg_ParseTuple() 檢查引數型別並轉換成C值。它使用模板字串檢測需要的引數型別。

PyArg_ParseTuple() 正常返回非零,並已經按照提供的地址存入了各個變數值。如果出錯(零)則應該讓函式返回NULL以通知直譯器出錯。

2   關於錯誤和異常
一個常見慣例是,函式發生錯誤時,應該設定一個異常環境並返回錯誤值(NULL)。異常儲存在直譯器靜態全域性變數中,如果為NULL,則沒有發生異常。異常的第一個引數也需要儲存在靜態全域性變數中,也就是raise的第二個引數。第三個變數包含棧回溯資訊。這三個變數等同於Python變數 sys.exc_type 、 sys.exc_value 、 sys.exc_traceback 。這對找到錯誤是很必要的。

Python API中定義了一些函式來設定這些變數。

最常用的就是 PyErr_SetString() 。引數是異常物件和C字串。異常物件一般由像 PyExc_ZeroDivisionError 這樣的物件來預定義。C字串指明異常原因,並最終儲存在異常的第一個引數裡面。

另一個有用的函式是 PyErr_SetFromErrno() ,僅接受一個異常物件,異常描述包含在全域性變數 errno 中。最通用的函式還是 PyErr_SetObject() ,包含兩個引數,分別為異常物件和異常描述。你不需要使用 Py_INCREF() 來增加傳遞到其他函式的引數物件的引用計數。

你可以通過 PyErr_Occurred() 獲知當前異常,返回當前異常物件,如果確實沒有則為NULL。一般來說,你在呼叫函式時不需要呼叫 PyErr_Occurred() 檢查是否發生了異常,你可以直接檢查返回值。

如果呼叫更下層函式時出錯了,那麼本函式返回NULL表示錯誤,並且整個呼叫棧中只要有一處呼叫 PyErr_*() 函式設定異常就可以。一般來說,首先發現錯誤的函式應該設定異常。一旦這個錯誤到達了Python直譯器的主迴圈,則會中斷當前執行程式碼並追究異常。

有一種情況下,模組可能依靠其他 PyErr_*() 函式給出更加詳細的錯誤資訊,並且是正確的。但是按照一般規則,這並不重要,很多操作都會因為種種原因而掛掉。

想要忽略這些函式設定的異常,異常情況必須明確的使用 PyErr_Clear() 來清除。只有在C程式碼想要自己處理異常而不是傳給直譯器時才這麼做。

每次失敗的 malloc() 呼叫必須丟擲一個異常,直接呼叫 malloc() 或 realloc() 的地方要呼叫 PyErr_NoMemory() 並返回錯誤。所有建立物件的函式都已經實現了這個異常的丟擲,所以這是每個分配記憶體都要做的。

還要注意的是 PyArg_ParseTuple() 系列函式的異常,返回一個整數狀態碼是有效的,0是成功,-1是失敗,有如Unix系統呼叫。

最後,小心垃圾情理,也就是 Py_XDECREF() 和 Py_DECREF() 的呼叫,會返回的異常。

選擇丟擲哪個異常完全是你的個人愛好了。有一系列的C物件代表了內建Python異常,例如 PyExc_ZeroDivisionError ,你可以直接使用。當然,你可能選擇更合適的異常,不過別使用 PyExc_TypeError 告知檔案開啟失敗(有個更合適的 PyExc_IOError )。如果引數列表有誤, PyArg_ParseTuple() 通常會丟擲 PyExc_TypeError 。如果引數值域有誤, PyExc_ValueError 更合適一些。

你也可以為你的模組定義一個唯一的新異常。需要在檔案前部宣告一個靜態物件變數,如:

static PyObject* SpamError;
然後在模組初始化函式(initspam())裡面初始化它,並省卻了處理:

PyMODINIT_FUNCinitspam(void) {    PyObject* m;    m=Py_InitModule("spam",SpamMethods);    if (m==NULL)        return NULL;    SpamError=PyErr_NewException("spam.error",NULL,NULL);    Py_INCREF(SpamError);    PyModule_AddObject(m,"error",SpamError);}
注意實際的Python異常名字是 spam.error 。 PyErr_NewException() 函式使用Exception為基類建立一個類(除非是使用另外一個類替代NULL)。

同樣注意的是建立類儲存了SpamError的一個引用,這是有意的。為了防止被垃圾回收掉,否則SpamError隨時會成為野指標。

一會討論 PyMODINIT_FUNC 作為函式返回型別的用法。

3   回到例子
回到前面的例子,你應該明白下面的程式碼:

if (!PyArg_ParseTuple(args,"s",&command))    return NULL;
就是為了報告直譯器一個異常。如果執行正常則變數會拷貝到本地,後面的變數都應該以指標的方式提供,以方便設定變數。本例中的command會被宣告為 “const char* command” 。

下一個語句使用UNIX系統函式system(),傳遞給他的引數是剛才從 PyArg_ParseTuple() 取出的:

sts=system(command);
我們的 spam.system() 函式必須返回一個PY物件,這可以通過 Py_BuildValue() 來完成,其形式與 PyArg_ParseTuple() 很像,獲取格式字串和C值,並返回新的Python物件:

return Py_BuildValue("i",sts);
在這種情況下,會返回一個整數物件,這個物件會在Python堆裡面管理。

如果你的C函式沒有有用的返回值,則必須返回None。你可以用 Py_RETUN_NONE 巨集來完成:

Py_INCREF(Py_None);return Py_None;
Py_None 是一個C名字指定Python物件None。這是一個真正的PY物件,而不是NULL指標。

4   模組方法表和初始化函式
把函式宣告為可以被Python呼叫,需要先定義一個方法表:

static PyMethodDef SpamMethods[]= {    ...    {"system",spam_system,METH_VARARGS,    "Execute a shell command."},    ...    {NULL,NULL,0,NULL}    /*必須的結束符*/};
注意第三個引數 METH_VARARGS ,這個標誌指定會使用C的呼叫慣例。可選值有 METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值0代表使用 PyArg_ParseTuple() 的陳舊變數。

如果單獨使用 METH_VARARGS ,函式會等待Python傳來tuple格式的引數,並最終使用 PyArg_ParseTuple() 進行解析。

METH_KEYWORDS 值表示接受關鍵字引數。這種情況下C函式需要接受第三個 PyObject* 物件,表示字典引數,使用 PyArg_ParseTupleAndKeywords() 來解析出引數。

方法表必須傳遞給模組初始化函式。初始化函式函式名規則為 initname() ,其中 name 為模組名。並且不能定義為檔案中的static函式:

PyMODINIT_FUNCinitspam(void) {    (void) Py_InitModule("spam",SpamMethods);}
注意 PyMODINIT_FUNC 聲明瞭void為返回型別,還有就是平臺相關的一些定義,如C++的就要定義成 extern “C” 。

Python程式首次匯入這個模組時就會呼叫initspam()函式。他呼叫 Py_InitModule() 來建立一個模組物件,同時這個模組物件會插入到 sys.modules 字典中的 “spam” 鍵下面。然後是插入方法表中的內建函式到 “spam” 鍵下面。 Py_InitModule() 返回一個指標指向剛建立的模組物件。他是有可能發生嚴重錯誤的,也有可能在無法正確初始化時返回NULL。

當嵌入Python時, initspam() 函式不會自動被呼叫,除非在入口處的 _PyImport_Inittab 表。最簡單的初始化方法是在 Py_Initialize() 之後靜態呼叫 initspam() 函式:

intmain(int argc, char* argv[]) {    Py_SetProgramName(argv[0]);    Py_Initialize();    initspam();    //...}
在Python發行版的 Demo/embed/demo.c 中有可以參考的原始碼。

Note

從 sys.modules 中移除模組入口,或者在多直譯器環境中匯入編譯模組,會導致一些擴充套件模組出錯。擴充套件模組作者應該特別注意初始化內部資料結構。同時要注意 reload() 函式可能會被用在擴充套件模組身上,並呼叫模組初始化函式,但是對動態狀如物件(動態連結庫),卻不會重新載入。

更多關於模組的現實的例子包含在Python原始碼包的Modules/xxmodule.c中。這些檔案可以用作你的程式碼模板,或者學習。指令碼 modulator.py 包含在原始碼發行版或Windows安裝中,提供了一個簡單的GUI,用來宣告需要實現的函式和物件,並且可以生成供填入的模板。指令碼在 Tools/modulator/ 目錄。檢視README以瞭解用法。

5   編譯和連線
如果使用動態載入,細節依賴於系統,檢視關於構建擴充套件模組部分,和關於在Windows下構建擴充套件的細節。

如果你無法使用動態載入,或者希望模組成為Python的永久組成部分,就必須改變配置並重新構建直譯器。幸運的是,這對UNIX來說很簡單,只要把你的程式碼(例如spammodule.c)放在 Modules/ Python原始碼目錄下,然後增加一行到檔案 Modules/Setup.local 來描述你的檔案即可:

spam spammodule.o
然後重新構建直譯器,使用make。你也可以在 Modules/ 子目錄使用make,但是你接下來首先要重建Makefile檔案,使用 make Makefile 命令。這對你改變 Setup 檔案來說很重要。

如果你的模組需要其他擴充套件模組連線,則需要在配置檔案後面加入,如:

spam spammodule.o -lX11
6   在C中呼叫Python函式
迄今為止,我們一直把注意力集中於讓Python呼叫C函式,其實反過來也很有用,就是用C呼叫Python函式。這在回撥函式中尤其有用。如果一個C介面使用回撥,那麼就要實現這個回撥機制。

幸運的是,Python直譯器是比較方便回撥的,並給標準Python函式提供了標準介面。這裡就不再詳述解析Python程式碼作為輸入的方式,如果有興趣可以參考 Python/pythonmain.c 中的 -c 命令程式碼。

呼叫Python函式,首先Python程式要傳遞Python函式物件。當呼叫這個函式時,用全域性變數儲存Python函式物件的指標,還要呼叫 Py_INCREF() 來增加引用計數,當然不用全域性變數也沒什麼關係。例如如下:

static PyObject* my_callback=NULL;static PyObject*my_set_callback(PyObject* dummy, PyObject* args) {    PyObject* result=NULL;    PyObject* temp;    if (PyArg_ParseTuple(args,"O:set_callback",&temp)) {        if (!PyCallable_Check(temp)) {            PyErr_SetString(PyExc_TypeError,"parameter must be callable");            return NULL;        }        Py_XINCREF(temp);        Py_XINCREF(my_callback);        my_callback=temp;        Py_INCREF(Py_None);        result=Py_None;    }    return result;}
這個函式必須使用 METH_VARARGS 標誌註冊到直譯器。巨集 Py_XINCREF() 和 Py_XDECREF() 增加和減少物件的引用計數。

然後,就要呼叫函數了,使用 PyEval_CallObject() 。這個函式有兩個引數,都是指向Python物件:Python函式和引數列表。引數列表必須總是tuple物件,如果沒有引數則要傳遞空的tuple。使用 Py_BuildValue() 時,在圓括號中的引數會構造成tuple,無論有沒有引數,如:

int arg;PyObject* arglist;PyObject* result;//...arg=123;//...arglist=Py_BuildValue("(i)",arg);result=PyEval_CallObject(my_callback,arglist);Py_DECREF(arglist);
PyEval_CallObject() 返回一個Python物件指標表示返回值。 PyEval_CallObject() 是 引用計數無關 的,有如例子中,引數列表物件使用完成後就立即減少引用計數了。`PyEval_CallObject()` 返回一個Python物件指標表示返回值。 PyEval_CallObject() 是 引用計數無關 的,有如例子中,引數列表物件使用完成後就立即減少引用計數了。

PyEval_CallObject() 的返回值總是新的,新建物件或者是對已有物件增加引用計數。所以你必須獲取這個物件指標,在使用後減少其引用計數,即便是對返回值沒有興趣也要這麼做。但是在減少這個引用計數之前,你必須先檢查返回的指標是否為NULL。如果是NULL,則表示出現了異常並中止了。如果沒有處理則會向上傳遞並最終顯示呼叫棧,當然,你最好還是處理好異常。如果你對異常沒有興趣,可以用 PyErr_Clear() 清除異常,例如:

if (result==NULL)    return NULL;  /*向上傳遞異常*///使用resultPy_DECREF(result);
依賴於具體的回撥函式,你還要提供一個引數列表到 PyEval_CallObject() 。在某些情況下引數列表是由Python程式提供的,通過介面再傳到回撥函式。這樣就可以不改變形式直接傳遞。另外一些時候你要構造一個新的tuple來傳遞引數。最簡單的方法就是 Py_BuildValue() 函式構造tuple。例如,你要傳遞一個事件物件時可以用:

PyObject* arglist;//...arglist=Py_BuildValue("(l)",eventcode);result=PyEval_CallObject(my_callback,arglist);Py_DECREF(arglist);if (result==NULL)    return NULL;  /*一個錯誤*//*使用返回值*/Py_DECREF(result);
注意 Py_DECREF(arglist) 所在處會立即呼叫,在錯誤檢查之前。當然還要注意一些常規的錯誤,比如 Py_BuildValue() 可能會遭遇記憶體不足等等。

7   解析傳給擴充套件模組函式的引數
函式 PyArg_ParseTuple() 宣告如下:

int PyArg_ParseTuple(PyObject* arg, char* format, ...);
引數 arg 必須是一個tuple物件,包含傳遞過來的引數, format 引數必須是格式化字串,語法解釋見 “Python C/API” 的5.5節。剩餘引數是各個變數的地址,型別要與格式化字串對應。

注意 PyArg_ParseTuple() 會檢測他需要的Python引數型別,卻無法檢測傳遞給他的C變數地址,如果這裡出錯了,可能會在記憶體中隨機寫入東西,小心。

任何Python物件的引用,在呼叫者這裡都是 借用的引用 ,而不增加引用計數。

一些例子:

int ok;int i,j;long k,l;const char* s;int size;ok=PyArg_ParseTuple(args,"");/* python call: f() */ok=PyArg_ParseTuple(args,"s",&s);/* python call: f('whoops!') */ok=PyArg_ParseTuple(args,"lls",&k,&l,&s);/* python call: f(1,2,'three') */ok=PyArg_ParseTuple(args,"(ii)s#",&i,&j,&s,&size);/* python call: f((1,2),'three') */{    const char* file;    const char* mode="r";    int bufsize=0;    ok=PyArg_ParseTuple(args,"s|si",&file,&mode,&bufsize);    /* python call:        f('spam')        f('spam','w')        f('spam','wb',100000)    */}{    int left,top,right,bottom,h,v;    ok=PyArg_ParseTuple(args,"((ii)(ii))(ii)",        &left,&top,&right,&bottom,&h,&v);    /* python call: f(((0,0),(400,300)),(10,10)) */}{    Py_complex c;    ok=PyArg_ParseTuple(args,"D:myfunction",&c);    /* python call: myfunction(1+2j) */}
8   解析傳給擴充套件模組函式的關鍵字引數
函式 PyArg_ParseTupleAndKeywords() 宣告如下:

int PyArg_ParseTupleAndKeywords(PyObject* arg, PyObject* kwdict, char* format, char* kwlist[],...);
引數arg和format定義同 PyArg_ParseTuple() 。引數 kwdict 是關鍵字字典,用於接受執行時傳來的關鍵字引數。引數 kwlist 是一個NULL結尾的字串,定義了可以接受的引數名,並從左到右與format中各個變數對應。如果執行成功 PyArg_ParseTupleAndKeywords() 會返回true,否則返回false並丟擲異常。

Note

巢狀的tuple在使用關鍵字引數時無法生效,不在kwlist中的關鍵字引數會導致 TypeError 異常。

如下是使用關鍵字引數的例子模組,作者是 Geoff Philbrick ([email protected]):

#include "Python.h"static PyObject*keywdarg_parrot(PyObject* self, PyObject* args, PyObject* keywds) {    int voltage;    char* state="a stiff";    char* action="voom";    char* type="Norwegian Blue";    static char* kwlist[]={"voltage","state","action","type",NULL};    if (!PyArg_ParseTupleAndKeywords(args,keywds,"i|sss",kwlist,            &voltage,&state,&action,&type))        return NULL;    printf("-- This parrot wouldn't %s if you put %i Volts through it.n",action,voltage);    printf("-- Lovely plumage, the %s -- It's %s!n",type,state);    Py_INCREF(Py_None);    return Py_None;}static PyMethodDef keywdary_methods[]= {    /*注意PyCFunction,這對需要關鍵字引數的函式很必要*/    {"parrot",(PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,"Print a lovely skit to standard output."},    {NULL,NULL,0,NULL}};voidinitkeywdarg(void) {    Py_InitModule("keywdarg",keywdarg_methods);}
9   構造任意值
這個函式宣告與 PyArg_ParseTuple() 很相似,如下:

PyObject* Py_BuildValue(char* format, ...);
接受一個格式字串,與 PyArg_ParseTuple() 相同,但是引數必須是原變數的地址指標。最終返回一個Python物件適合於返回給Python程式碼。

一個與 PyArg_ParseTuple() 的不同是,後面可能需要的要求返回一個tuple,比如用於傳遞給其他Python函式以引數。 Py_BuildValue() 並不總是生成tuple,在多於1個引數時會生成tuple,而如果沒有引數則返回None,一個引數則直接返回該引數的物件。如果要求強制生成一個長度為空的tuple,或包含一個元素的tuple,需要在格式字串中加上括號。

例如:

程式碼 返回值
Py_BuildValue(”") None
Py_BuildValue(”i”,123) 123
Py_BuildValue(”iii”,123,456,789) (123,456,789)
Py_BuildValue(”s”,”hello”) ‘hello’
Py_BuildValue(”ss”,”hello”,”world”) (’hello’, ‘world’)
Py_BuildValue(”s#”,”hello”,4) ‘hell’
Py_BuildValue(”()”) ()
Py_BuildValue(”(i)”,123) (123,)
Py_BuildValue(”(ii)”,123,456) (123,456)
Py_BuildValue(”(i,i)”,123,456) (123,456)
Py_BuildValue(”[i,i]”,123,456) [123,456]
Py_BuildValue(”{s:i,s:i}”,’a',1,’b',2) {’a':1,’b':2}
Py_BuildValue(”((ii)(ii))(ii)”,1,2,3,4,5,6) (((1,2),(3,4)),(5,6))

10   引用計數
在C/C++語言中,程式設計師負責動態分配和回收堆(heap)當中的記憶體。這意味著,我們在C中程式設計時必須面對這個問題。

每個由 malloc() 分配的記憶體塊,最終都要由 free() 扔到可用記憶體池裡面去。而呼叫 free() 的時機非常重要,如果一個記憶體塊忘了 free() 則是記憶體洩漏,程式結束前將無法重新使用。而如果對同一記憶體塊 free() 了以後,另外一個指標再次訪問,則叫做野指標。這同樣會導致嚴重的問題。

記憶體洩露往往發生在一些並不常見的程式流程上面,比如一個函式申請了資源以後,卻提前返回了,返回之前沒有做清理工作。人們經常忘記釋放資源,尤其對於後加新加的程式碼,而且會長時間都無法發現。這些函式往往並不經常呼叫,而且現在大多數機器都有龐大的虛擬記憶體,所以記憶體洩漏往往在長時間執行的程序,或經常被呼叫的函式中才容易發現。所以最好有個好習慣加上程式碼約定來儘量避免記憶體洩露。

Python往往包含大量的記憶體分配和釋放,同樣需要避免記憶體洩漏和野指標。他選擇的方法就是 引用計數 。其原理比較簡單:每個物件都包含一個計數器,計數器的增減與引用的增減直接相關,當引用計數為0時,表示物件已經沒有存在的意義了,就可以刪除了。

一個叫法是 自動垃圾回收 ,引用計數是一種垃圾回收方法,使用者必須要手動呼叫 free() 函式。優點是可以提高記憶體使用率,缺點是C語言至今也沒有一個可移植的自動垃圾回收器。引用計數卻可以很好的移植,有如C當中的 malloc() 和 free() 一樣。也許某一天會出現C語言餓自動垃圾回收器,不過在此之前我們還得用引用計數。

Python使用傳統的引用計數實現,不過他包含一個迴圈引用探測器。這允許應用不需要擔心的直接或間接的建立迴圈引用,而這實際上是引用計數實現的自動垃圾回收的致命缺點。迴圈引用指物件經過幾層引用後回到自己,導致了其引用計數總是不為0。傳統的引用計數實現無法解決迴圈引用的問題,儘管已經沒有其他外部引用了。

迴圈引用探測器可以檢測出垃圾回收中的迴圈並釋放其中的物件。只要Python物件有 __del__() 方法,Python就可以通過 gc module 模組來自動暴露出迴圈引用。gc模組還提供 collect() 函式來執行迴圈引用探測器,可以在配置檔案或執行時禁用迴圈應用探測器。

迴圈引用探測器作為一個備選選項,預設是開啟的,可以在構建時使用 –without-cycle-gc 選項加到 configure 上來配置,或者移除 pyconfig.h 檔案中的 WITH_CYCLE_GC 巨集定義。在迴圈引用探測器禁用後,gc模組將不可用。

10.1   Python中的引用計數
有兩個巨集 Py_INCREF(x) 和 Py_DECREF(x) 用於增減引用計數。 Py_DECREF() 同時會在引用計數為0時釋放物件資源。為了靈活性,他並不是直接呼叫 free() 而是呼叫物件所在型別的解構函式。

一個大問題是何時呼叫 Py_INCREF(x) 和 Py_DECREF(x) 。首先介紹一些術語。沒有任何人都不會 擁有 一個物件,只能擁有其引用。對一個物件的引用計數定義了引用數量。擁有的引用,在不再需要時負責呼叫 Py_DECREF() 來減少引用計數。傳遞引用計數有三種方式:傳遞、儲存和呼叫 Py_DECREF() 。忘記減少擁有的引用計數會導致記憶體洩漏。

同樣重要的一個概念是 借用 一個物件,借用的物件不能呼叫 Py_DECREF() 來減少引用計數。借用者在不需要借用時,不保留其引用就可以了。應該避免擁有者釋放物件之後仍然訪問物件,也就是野指標。

借用的優點是你無需管理引用計數,缺點是可能被野指標搞的頭暈。借用導致的野指標問題常發生在看起來無比正確,但是事實上已經被釋放的物件。

借用的引用也可以用 Py_INCREF() 來改造成擁有的引用。這對引用的物件本身沒什麼影響,但是擁有引用的程式有責任在適當的時候釋放這個擁有。

10.2   擁有規則
一個物件的引用進出一個函式時,其引用計數也應該同時改變。

大多數函式會返回一個對物件擁有的引用。而且幾乎所有的函式其實都會建立一個物件,例如 PyInt_FromLong() 和 Py_BuildValue() ,傳遞一個擁有的引用給接受者。即便不是剛建立的,你也需要接受一個新的擁有引用。一般來說, PyInt_FromLong() 會維護一個常用值快取,並且返回快取項的引用。

很多函式提取一些物件的子物件並傳遞擁有引用,例如 PyObject_GetAttrString() 。另外,小心一些函式,包括: PyTuple_GetItem() 、 PyList_GetItem() 、 PyDict_GetItem() 和 PyDict_GetItemString() ,他們返回的都是借用的引用。

函式 PyImport_AddModule() 也是返回借用的引用,儘管他實際上建立了物件,只不過其擁有的引用實際儲存在了 sys.modules 中。

當你傳遞一個物件的引用到另外一個函式時,一般來說,函式是借用你的引用,如果他確實需要儲存,則會使用 Py_INCREF() 來變為擁有引用。這個規則有兩種可能的異常: PyTuple_SetItem() 和 PyList_SetItem() ,這兩個函式獲取傳遞給他的擁有引用,即便是他們執行出錯了。不過 PyDict_SetItem() 卻不是接收擁有的引用。

當一個C函式被py呼叫時,使用對引數的借用。呼叫者擁有引數物件的擁有引用。所以,借用的引用的壽命是函式返回。只有當這類引數必須儲存時,才會使用 Py_INCREF() 變為擁有的引用。

從C函式返回的物件引用必須是擁有的引用,這時的擁有者是呼叫者。

10.3   危險的薄冰
有些使用借用的情況會出現問題。這是對直譯器的盲目理解所導致的,因為擁有者往往提前釋放了引用。

首先而最重要的情況是使用 Py_DECREF() 來釋放一個本來是借用的物件,比如列表中的元素:

voidbug(PyObject* list) {    PyObject* item=PyList_GetItem(list,0);    PyList_SetItem(list,1,PyInt_FromLong(0L));    PyObject_Print(item,stdout,0); /* BUG! */}
這個函式首先借用了 list[0] ,然後把 list[1] 替換為值0,最後列印借用的引用。看起來正確麼,不是!

我們來跟蹤一下 PyList_SetItem() 的控制流,列表擁有所有元素的引用,所以當專案1被替換時,他就釋放了原始專案1。而原始專案1是一個使用者定義類的例項,假設這個類定義包含 __del__() 方法。如果這個類的例項引用計數為1,處理過程會呼叫 __del__() 方法。

因為使用python編寫,所以 __del__() 中可以用任何python程式碼來完成釋放工作。替換元素的過程會執行 del list[0] ,即減掉了物件的最後一個引用,然後就可以釋放記憶體了。

知道問題後,解決方案就出來了:臨時增加引用計數。正確的版本如下:

voidno_bug(PyObject* list) {    PyObject* item=PyList_GetItem(list,0);    Py_INCREF(item);    PyList_SetItem(list,1,PyInt_FromLong(0L));    PyObject_Print(item,stdout,0);    Py_DECREF(item);}
這是一個真實的故事,舊版本的Python中多處包含這個問題,讓guido花費大量時間研究 __del__() 為什麼失敗了。

第二種情況的問題出現在多執行緒中的借用引用。一般來說,python中的多執行緒之間並不能互相影響對方,因為存在一個GIL。不過,這可能使用巨集 Py_BEGIN_ALLOW_THREADS 來臨時釋放鎖,最後通過巨集 Py_END_ALLOW_THREADS 來再申請鎖,這在IO呼叫時很常見,允許其他執行緒使用處理器而不是等待IO結束。很明顯,下面的程式碼與前面的問題相同:

voidbug(PyObject* list) {    PyObject* item=PyList_GetItem(list,0);    Py_BEGIN_ALLOW_THREADS    //一些IO阻塞呼叫    Py_END_ALLOW_THREADS    PyObject_Print(item,stdout,0); /*BUG*/}
10.4   NULL指標
一般來說,函式接受的引數並不希望你傳遞一個NULL指標進來,這會出錯的。函式的返回物件引用返回NULL則代表發生了異常。這是Python的機制,畢竟,一個函式如果執行出錯了,那麼也沒有必要多解釋了,浪費時間。(注:彪悍的異常也不需要解釋)

最好的測試NULL的方法就是在程式碼裡面,一個指標如果收到了NULL,例如 malloc() 或其他函式,則表示發生了異常。

巨集 Py_INCREF() 和 Py_DECREF() 並不檢查NULL指標,不過還好, Py_XINCREF() 和 Py_XDECREF() 會檢查。

檢查特定型別的巨集,形如 Pytype_Check() 也不檢查NULL指標,因為這個檢查是多餘的。

C函式的呼叫機制確保傳遞的引數列表(也就是args引數)用不為NULL,事實上,它總是一個tuple。

而把NULL扔到Python使用者那裡可就是一個非常嚴重的錯誤了。

11   使用C++編寫擴充套件
有時候需要用C++編寫Python擴充套件模組。不過有一些嚴格的限制。如果Python直譯器的主函式是使用C編譯器編譯和連線的,那麼全域性和靜態物件的建構函式將無法使用。而主函式使用C++編譯器時則不會有這個問題。被Python呼叫的函式,特別是模組初始化函式,必須宣告為 extern “C” 。沒有必要在Python標頭檔案中使用 extern “C” 因為在使用C++編譯器時會自動加上 __cplusplus 這個定義,而一般的C++編譯器一般都會設定這個符號。

12   提供給其他模組以C API
很多模組只是提供給Python使用的函式和新型別,但是偶爾也有可能被其他擴充套件模組所呼叫。例如一個模組實現了 “collection” 型別,可以像list一樣工作而沒有順序。有如標準Python中的list型別一樣,提供的C介面可以讓擴充套件模組建立和管理list,這個新的型別也需要有C函式以供其他擴充套件模組直接管理。

初看這個功能可能以為很簡單:只要寫這些函式就行了(不需要宣告為靜態),提供適當的標頭檔案,並註釋C的API。當然,如果所有的擴充套件模組都是靜態連結到Python直譯器的話,這當然可以正常工作。但是當其他擴充套件模組是動態連結庫時,定義在一個模組中的符號,可能對另外一個模組來說並不是可見的。而這個可見性又是依賴作業系統實現的,一些作業系統對Python直譯器使用全域性名稱空間和所有的擴充套件模組(例如Windows),也有些系統則需要明確的宣告模組的匯出符號表(AIX就是個例子),或者提供一個不同策略的選擇(大多數的Unices)。即便這些符號是全域性可見的,擁有函式的模組,也可能尚未載入。

為了可移植性,不要奢望任何符號會對外可見。這意味著模組中所有的符號都宣告為 static ,除了模組的初始化函式以外,這也是為了避免各個擴充套件模組之間的符號名稱衝突。這也意味著必須以其他方式匯出擴充套件模組的符號。

Python提供了一種特殊的機制,以便在擴充套件模組間傳遞C級別的資訊(指標): CObject 。一個CObject是一個Python的資料型別,儲存了任意型別指標(void*)。CObject可以只通過C API來建立和存取,但是卻可以像其他Python物件那樣來傳遞。在特別的情況下,他們可以被賦予一個擴充套件模組名稱空間內的名字。其他擴充套件模組隨後可以匯入這個模組,獲取這個名字的值,然後得到CObject中儲存的指標。

通過CObject有很多種方式匯出擴充套件模組的C API。每個名字都可以得到他自己的CObject,或者可以把所有的匯出C API放在一個CObject指定的陣列中來發布。所以可以有很多種方法匯出C API。

如下的示例程式碼展示了把大部分的重負載任務交給擴充套件模組,作為一個很普通的擴充套件模組的例子。他儲存了所有的C API的指標到一個數組中,而這個陣列的指標儲存在CObject中。對應的標頭檔案提供了一個巨集以管理匯入模組和獲取C API的指標,客戶端模組只需要在存取C API之前執行這個巨集就可以了。

這個匯出模組是修改自1.1節的spam模組。函式 spam.system() 並不是直接呼叫C庫的函式 system() ,而是呼叫 PySpam_System() ,提供了更加複雜的功能。這個函式 PySpam_System() 同樣匯出供其他擴充套件模組使用。

函式 PySpam_System() 是一個純C函式,宣告為static如下:

static intPySpam_System(const char* command) {    return system(command);}
函式 spam_system() 做了細小的修改:

static PyObject*spam_system(PyObject* self, PyObject* args) {    const char* command;    int sts;    if (!PyArg_ParseTuple(args,"s",&command))        return NULL;    sts=PySpam_System(command);    return Py_BuildValue("i",sts);}
在模組的頭部加上如下行:

#include "Python.h"
另外兩行需要新增的是:

#define SPAM_MODULE#include "spammodule.h"
這個巨集定義是告訴標頭檔案需要作為匯出模組,而不是客戶端模組。最終模組的初始化函式必須管理初始化C API指標陣列的初始化:

PyMODINIT_FUNCinitspam(void){    PyObject *m;    static void *PySpam_API[PySpam_API_pointers];    PyObject *c_api_object;    m = Py_InitModule("spam", SpamMethods);    if (m == NULL)        return;    /* Initialize the C API pointer array */    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;    /* Create a CObject containing the API pointer array's address */    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);    if (c_api_object != NULL)        PyModule_AddObject(m, "_C_API", c_api_object);}
注意 PySpam_API 宣告為static,否則 initspam() 函式執行之後,指標陣列就消失了。

大部分的工作還是在標頭檔案 spammodule.h 中,如下:

#ifndef Py_SPAMMODULE_H#define Py_SPAMMODULE_H#ifdef __cplusplusextern "C" {#endif/* Header file for spammodule *//* C API functions */#define PySpam_System_NUM 0#define PySpam_System_RETURN int#define PySpam_System_PROTO (const char *command)/* Total number of C API pointers */#define PySpam_API_pointers 1#ifdef SPAM_MODULE/* This section is used when compiling spammodule.c */static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;#else/* This section is used in modules that use spammodule's API */static void **PySpam_API;#define PySpam_System  (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])/* Return -1 and set exception on error, 0 on success. */static intimport_spam(void){    PyObject *module = PyImport_ImportModule("spam");    if (module != NULL) {        PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API");        if (c_api_object == NULL)            return -1;        if (PyCObject_Check(c_api_object))            PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object);        Py_DECREF(c_api_object);    }    return 0;}#endif#ifdef __cplusplus}#endif#endif /* !defined(Py_SPAMMODULE_H) */
想要呼叫 PySpam_System() 的客戶端模組必須在初始化函式中呼叫 import_spam() 以初始化匯出擴充套件模組:

PyMODINIT_FUNCinitclient(void) {    PyObject* m;    m=Py_InitModule("client",ClientMethods);    if (m==NULL)        return;    if (import_spam()<0)        return;    /*其他初始化語句*/}
這樣做的缺點是 spammodule.h 有點複雜。不過這種結構卻可以方便的用於其他匯出函式,所以學著用一次也就好了。

最後需要提及的是CObject提供的一些附加函式,用於CObject指定的記憶體塊的分配和釋放。詳細資訊可以參考Python的C API參考手冊的CObject一節,和CObject的實現,參考檔案 Include/cobject.h 和 Objects/cobject.c 。


 
-
 
 
來頂一下
 
返回首頁
 
 

發表評論 共有0條評論
使用者名稱:  密碼: 
驗證碼:     匿名發表
     
 
 
 
相關文章
PHP中通過Web執行C/C++應用程式
如何在Ubuntu 7.10上實現C/C++開
c/c++中結構體(struct)知識點強化
c/c++中結構體的入門教程
C/C++中命令列引數的原理
C/C++中利用空指標(NULL),提高程
c/c++中的字元指標陣列,指向指標
c/c++中字串常量的不相等性,以
C/C++中陣列和指標型別的關係的入
C/C++中列舉型別(enum)的入門教程
 
欄目更新
C預編譯中關於位元組對齊的問題
getopt(3)手冊頁翻譯及其補充
linux C程式中獲取shell指令碼
編輯數值金額成中文金額
蟻群演算法小程式用C/C++語言實
假幣問題--C++解決方案
C/C++ 簡單的多執行緒程式設計
呼叫C++複製建構函式和拷貝構
用好c++的const 關鍵字
C標準型別的長度bytes
 
欄目熱門
C#傳送Email郵件方法總結
c語言static與extern的用法
VC++(Ctime日期函式)應用
Windows/Linux下配置Eclipse
C/C++ 簡單的多執行緒程式設計
C/C++對檔案操作
c++二叉樹實現原始碼
struct的初始化,拷貝及指標
typedef struct和struct的區
詳細解析C++虛擬函式表
 
 
站內搜尋:     Linux頻道 下載頻道 相簿 商品 嵌入式頻道   高階搜尋
 
網站首頁 | 欄目導航 | 服務條款 | 廣告服務 | 聯絡我們 | 網站大全 | 免責宣告 | 返回頂部
Copyright ? 2007-2008 xxlinux.com, All rights reserved.
Powered by linux聯盟 京ICP備05012402號
 
 

相關推薦

轉載關於Python混合程式設計記憶體洩露

       登陸論壇  | 論壇註冊| 加入收藏 | 設為首頁| RSS      首頁Linux頻道軟體下載開發語言嵌入式頻道開源論壇 | php | JSP | ASP | asp.net | JAVA | c/c++/c# | perl | JavaScrip

轉載@Python 程式設計師,如何實現狂拽酷炫的 3D 程式設計技術?

今天給大家介紹一位美麗的姑娘。她的名字叫Pyecharts,打從我第一眼見到她後,就深深地被她迷住,並且愛上了她。 簡單說一下她的來歷:Pyecharts是一款強大的視覺化工具。百度開發了一款基於JS強大的視覺化庫Echarts,可我們在繪圖時,通常並不使用前端的技術來整理資料,而轉換資料結構又

轉載python中math模塊常用的方法

sum tran magic 大於 mea 正弦 erlang his isnan 轉自:https://www.cnblogs.com/renpingsheng/p/7171950.html ceil #取大於等於x的最小的整數值,如果x是一個整數,則返回x ceil(x

轉載Python中的正則表達式教程

大小 區別 some 操作 按位或 出了 sta 技術分享 嘗試 本文http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html 正則表達式經常被用到,而自己總是記不全,轉載一份完整的以備不時之需。 1.

轉載Python操作Excel的讀取以及寫入

body .sh open 列數 讀取 efault jin rap ring 轉載來源:https://jingyan.baidu.com/article/e2284b2b754ac3e2e7118d41.html #導入包 import xlrd #設置路徑 path

轉載Python 中的 if __name__ == '__main__' 該如何理解

一個 知識 如果 協程 運行 pat 執行 開始 參考資料 轉自 曠世的憂傷 http://blog.konghy.cn/2017/04/24/python-entry-program/ 程序入口 對於很多編程語言來說,程序都必須要有一個入口,比如 C,C++,以及完全面向

轉載Python裝飾器-----Toby Qin

callable cache __init__ spec Go 自帶 pos 發揮 date Python中的裝飾器是你進入Python大門的一道坎,不管你跨不跨過去它都在那裏。 為什麽需要裝飾器 我們假設你的程序實現了say_hello()和say_goodbye()兩個

轉載python安裝numpy和pandas

nump 列數 tro pac nio libs hub linux環境 github 轉載:原文地址 http://www.cnblogs.com/lxmhhy/p/6029465.html 最近要對一系列數據做同比比較,需要用到numpy和pandas來計算,不過使

轉載python %s %d %f

number %d html out 綜合 bar cli OS 結果 %s 字符串 [python] view plain copy print? string="hello" #%s打印時結果是hello print "stri

轉載python模組之poplib: 用pop3收取郵件

轉載自: http://www.cnblogs.com/sislcb/archive/2008/12/01/1344858.html  python的poplib模組是用來從pop3收取郵件的,也可以說它是處理郵件的第一步。  &nbs

轉載Python字串操作之字串分割與組合

1、 str.split():字串分割函式   通過指定分隔符對字串進行切片,並返回分割後的字串列表。   語法:   str.split(s, num)[n]   引數說明:   s:表示指定的分隔符,不寫的話,預設是空格(’ ‘)。如果字串中沒有給定的分隔符時,則把整個字串作為列表的一個元素返回。   n

施工ing整理python---各種程式設計基礎概念---辨析整理

程式:程式只是基於“使用者輸入”而“執行的動作的一些指令”的集合。 Python裡只有3種不同的指令: (1)表示式:expression             由操作符連線的值      

轉載python 3.6整合安裝xadmin

安裝xadmin 通過pip進行安裝 pip install xadmin 1 安裝完成後,發現會自動把關聯的對應包給一起安裝上 ,但是在我們執行如下命令的時候會報錯: python manage.py

轉載python基礎-檔案讀寫'r' 與 'rb' 和‘r+'與’rb+'區別

【轉載連結:https://www.cnblogs.com/nulige/p/6128948.html】   一、Python檔案讀寫的幾種模式: r,rb,w,wb 那麼在讀寫檔案時,有無b標識的的主要區別在哪裡呢?   1、檔案使用方式標識

轉載Python tips: 什麽是*args和**kwargs?

**kwargs urn 必須 http return log post 什麽 nta 轉自Python tips: 什麽是*args和**kwargs? 先來看個例子: def foo(*args, **kwargs): print ‘args = ‘, args

轉載Python tips: 什麼是*args和**kwargs? Python tips: 什麼是*args和**kwargs?

轉自Python tips: 什麼是*args和**kwargs? 先來看個例子: def foo(*args, **kwargs): print 'args = ', args print 'kwargs = ', kwargs print '------------------

轉載python read(), readline(). readlines()

.read() 每次讀取整個檔案,它通常用於將檔案內容放到一個字串變數中。 然而 .read() 生成檔案內容最直接的字串表示,但對於連續的面向行的處理,它卻是不必要的,並且如果檔案大於可用記憶體,則不可能實現這種處理。 .readline() 和 .readlines() 之間的差異是

轉載python使用loggin和ConfigParser配置檔案中遇到的問題

原文連結:https://blog.csdn.net/weixin_39918285/article/details/79551104 問題一: 載入loggin配置檔案時出錯 UnicodeDecodeError 一開始logger.conf配置檔案的存檔格式為unicode,該檔案中有中文字

轉載Python中ConfigParser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: "%&'" 解決方案

原文連結:https://blog.csdn.net/s740556472/article/details/82889758 前言在寫python程式讀取配置檔案的時候,遇到了一個錯誤,記錄下解決方案。 錯誤如下: 程式碼詳情讀取read_ini.ini時由於我的ini檔案中內容如下: 當代碼

轉載Java併發程式設計:Lock

Java併發程式設計:Lock   在上一篇文章中我們講到了如何使用關鍵字synchronized來實現同步訪問。本文我們繼續來探討這個問題,從Java 5之後,在java.util.concurrent.locks包下提供了另外一種方式來實現同步訪問,那就是Lock。