【轉載】關於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。