先提示,本文需要一定的python源碼基礎。許多內容請參考《python源碼剖析》。下面切入正題。
今天在羣裏有人問了一個問題。形如如下的一段程序。
class person:
sum = 0
def __init__(self,name):
self.name=name
person.sum += 1
def __del__(self):
person.sum -= 1
print "%s is leaving" % self.name
a = person('a')
a2 = person('a2')
這段程序的預期的執行結果應該是"a is leaving"和"a2 is leaving"。但是實際上卻出乎意料之外,實際的執行結果如下:
a is leaving
Exception exceptions.AttributeError: "'NoneType' object has no attribute 'sum'" in
<bound method person.__del__ of <__main__.person instance at 0x4a18f0>> ignored
爲什麼引用的名字不同造成的結果會有這麼大的差別呢?
分析表面的原因,是person這個引用被指向了None。那他是不是真的是None呢?
def __del__(self):
print globals()['person'] #1
person.sum -= 1
#print "%s is leaving" % self.name
加入紅色這行代碼,看看是不是真的變成了None。運行結果如下:
__main__.person
None
Exception exceptions.AttributeError: "'NoneType' object has no attribute 'sum'"
in <bound method person.__del__ of <__main__.person instance at 0x4a18c8>> ignored
看來是真的變成了None了。
初步分析原因,應該是程序在執行結束以後,python虛擬機清理環境的時候將"person"這個符號先於"a2"清理了,所以導致在a2的析構函數中無法找到"person"這個符號了。
但是轉念一想還是不對,如果是"person"符號找不到了,應該是提示“name 'person' is not defined”纔對。說明"person"這個符號還在,那"person"指向的class_object對象還在嗎?改變程序爲以下格式:
class person:
sum = 0
def __init__(self,name):
self.name=name
person.sum += 1
def __del__(self):
#person.sum -= 1
self.__class__.sum -= 1 #1
#print "%s is leaving" % self.name
a = person('a')
a2 = person('a2')
紅色代碼就是修改部分,利用自身的__class__來操作。運行結果一切正常。
說明python虛擬機在回收的過程中,只是將"person"這個符號設置成None了。這個結論同時帶來2個問題:第一,爲什麼會設置成None?第二:爲什麼"person"會先於"a2"而晚於"a"被回收?
先來分析第二個問題。第一反應是不是按照字母的順序來回收?但是馬上這個結論被推翻。"a"和"a2"都在"person"的前面。那麼難道是根據globals()這個字典的key順序來回收?執行一下globals().keys()方法,得到以下結果:
['a', '__builtins__', '__file__', 'person', 'a2', '__name__', '__doc__']
看來的確是這樣。
但是爲什麼是這樣?要得出這個結論,看來只有去python源碼中找答案了。
大家都知道,python代碼在運行的時候,會存在一個frameobject對象來表示運行時的環境。類似於c語言的棧幀,也有點像lisp的函數的生存空間,看起來答案要從frameobject.c這個文件中去找了。
在frameobject.c中發現了一個函數:static void frame_dealloc(PyFrameObject *f)。看來解決問題的關鍵就在眼前。
在frame_dealloc裏面截取了以下一段代碼:
Py_XDECREF(f->f_back);
Py_DECREF(f->f_builtins);
Py_DECREF(f->f_globals);
Py_CLEAR(f->f_locals);
Py_CLEAR(f->f_trace);
Py_CLEAR(f->f_exc_type);
Py_CLEAR(f->f_exc_value);
Py_CLEAR(f->f_exc_traceback);
原來減少了引用啊。。關於Py_DECREF這個宏,python源碼裏面的解釋是這樣的:
reference counts. Py_DECREF calls the object's deallocator function when
the refcount falls to 0;
這麼說來,我們就要去找f_globals的析構函數了。f_globals是個什麼呢?當然是PyDictObject了。證據麼遍地都是啊,比如隨手找了一個,在PyFrameObject * PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,PyObject *locals)這個函數裏面有一段代碼:
#ifdef Py_DEBUG
if (code == NULL || globals == NULL || !PyDict_Check(globals) ||
(locals != NULL && !PyMapping_Check(locals))) {
PyErr_BadInternalCall();
return NULL;
}
#endif
PyDict_Check。。。檢查是否是Dict對象。好吧,此處略過,直接奔向dictobject.c看看裏面的代碼。
static void
dict_dealloc(register dictobject *mp)
{
register dictentry *ep;
Py_ssize_t fill = mp->ma_fill;
PyObject_GC_UnTrack(mp);
Py_TRASHCAN_SAFE_BEGIN(mp)
for (ep = mp->ma_table; fill > 0; ep++) {
if (ep->me_key) {
--fill;
Py_DECREF(ep->me_key); #
Py_XDECREF(ep->me_value); #僅僅只是引用計數減一
}
}
以下略
哈哈哈。還真是按照key的順序來一個一個清除的。
不過,怎麼又回到了Py_DECREF啊?
看來最終解釋這個問題要回到GC上面了。
其實從這個地方也可以看出第一個問題的答案了,爲什麼是None?
從上面代碼可以看出,dictobject對象在析構的時候,僅僅只是將value的引用計數減一,至於這個對象什麼時候被真正回收,其實是由GC決定而不確定的。也就是說爲什麼是None,是因爲減一了以後,湊巧GC到了而已。
根據Python本身的文檔。
Python不能保證__del__被調用的時候所有的引用都有,所以,儘量不要overried類的__del__方法。
到此結束。