Python中globals對象的回收順序分析

先提示,本文需要一定的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源碼裏面的解釋是這樣的:

 

The macros Py_INCREF(op) and Py_DECREF(op) are used to increment or decrement
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 寫道
Warning: Due to the precarious circumstances under which __del__() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead. Also, when __del__() is invoked in response to a module being deleted (e.g., when execution of the program is done), other globals referenced by the __del__() method may already have been deleted. For this reason, __del__() methods should do the absolute minimum needed to maintain external invariants. Starting with version 1.5, Python guarantees that globals whose name begins with a single underscore are deleted from their module before other globals are deleted; if no other references to such globals exist, this may help in assuring that imported modules are still available at the time when the __del__() method is called.
 

Python不能保證__del__被調用的時候所有的引用都有,所以,儘量不要overried類的__del__方法。

 

到此結束。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章