2010年12月12日 星期日

Q&A -- 使用__dict__["__setattr__"]取代現有的類別函數

Q:

操作物件內的__dict__字典相當於操作物件內的屬性存取. 雖然仍有些不同.
obj.__dict__[name] = value # obj.name = value

然而以 obj.__dict__["__setattr__"] = new_fn 來取代現有的 __setattr__ 時
遇到一個問題(使用python2.6):

class aKlass:
    var = 0
    def a_fn(self):
        print "aKlass.a_fn"
    def __setattr__(self, name, value):
        print "aKlass.__setattr__",name,value
                                                                                
def new_a_fn(self):
    print "new_a_fn"
def new_setattr_fn(self,name,value):
    print "new_setattr_fn",name,value

aKlass內含兩個函數, a_fn, __setattr__.
另準備兩個對應的函數new_a_fn, new_setattr_fn
希望使用aKlass.__dict__[ ]的方式取代 --

aObj = aKlass()
aObj.a_fn()
aKlass.__dict__["a_fn"] = new_a_fn
aObj.a_fn()
aObj.var = 1
aKlass.__dict__["__setattr__"] = new_setattr_fn
aObj.var = 1


執行結果:

aKlass.a_fn
new_a_fn
aKlass.__setattr__ var 1
aKlass.__setattr__ var 1


aKlass.__dict__["a_fn"] = new_a_fn 的確用new_a_fn取代了原先的a_fn.
但似乎 aKlass.__dict__["__setattr__"] = new_setattr_fn 沒有達到預期的效果.
不知原因為何?

(雖然使用 setattr(aKlass,"__setattr__",new_setattr_fn) 就可以取代原函數了.)



A:

你測試的兩個 case 並不算是全等的,如果讓兩者在做法更接近,應該是:
aKlass.__dict__["a_fn"] = new_a_fn
aObj.a_fn()
aKlass.__dict__["__setattr__"] = new_setattr_fn
aObj.__setattr__('var', 1)

接下來回到為什麼修改 aKlass.__dict__ 後,在如此的 statement:
aObj.var = 1
上看不到作用?

1. 這只會發生在 old-style class。
(new-style class 的 __dict__ 則是 immutable,實際上是個 proxy)

2. aKlass object(不是指 aObj)除了 __dict__ 裡有 reference 指涉原本在
constructing aKlass 過程中所建立的 __setattr__ function(這個 function
是指 aKlass.__setattr__.im_func),aKlass object 本身也有一個欄位存著
該 function 的位址。(可以看看 Python 安裝後一併附的 classobject.h)

我把 CPython 2.6.5 的 classobject.h 部分內容節錄在此:
typedef struct {
    PyObject_HEAD
    PyObject    *cl_bases;      /* A tuple of class objects */
    PyObject    *cl_dict;       /* A dictionary */
    PyObject    *cl_name;       /* A string */
    /* The following three are functions or NULL */
    PyObject    *cl_getattr;
    PyObject    *cl_setattr;
    PyObject    *cl_delattr;
} PyClassObject;

aKlass 在記憶體裡的結構會分別有 __getattr__/__setattr__/__delattr__
的位址(if any)。
當執行過此 statement:

aKlass.__dict__['__setattr__'] = new_setattr

只變更了 cl_dict 所指涉的 dict 的狀態,並沒有變更 cl_setattr 指向另一個
function,而 aObj.var = 1 statement 會直接使用 cl_setattr 的值(一種優化,
省了 dictionary lookup 動作),所以 aObj.var = 1 還是調用了原來的
__setattr__ function。

如果你使用下列的 statement(任一)來變更 __setattr__ 屬性:
aKlass.__setattr__ = new_setattr_fn
setattr(aKlass, '__setattr__', new_setattr_fn)

則會變更 cl_setattr 欄位指向 new_setattr_fn,所以 aObj.var = 1
有預期中的表現。

接下來我要透過一些手法來變更 aKlass 在記憶體中其 cl_setattr 欄位的值,
看看有什麼效果。
* 有心一試的人請在 console mode 執行 python REPL(interpreter),不要
使用 IDLE 或是 PythonWin Editor 等 IDE 環境。

沿用 aKlass/new_a_fn/new_setattr_fn 的定義。
接下來 import ctypes 套件(已內建於 Python 2.5+)

from ctypes import *
a = aKlass()
wrapper = py_object(aKlass)
p = cast(addressof(wrapper), POINTER(POINTER(c_uint32))).contents
                                                                                
print id(aKlass.__setattr__.im_func) # address of original __setattr__
                                     # function
for x in xrange(10):
    print x, p[x]

執行完以上的程式碼後,大概可以看到類似如下的 output:
12325616
0 3
1 505362952
2 11210800
3 11416608
4 12313696
5 0
6 12325616
7 0
8 7
9 505362336

這表示 p[6] 是 cl_setattr 欄位(p[5] 是 cl_getattr,p[7] 是 cl_delattr,
兩者皆是 0,因為 aKlass 沒有定義 __getattr__, __delattr__)。

old_setattr_addr = p[6]    # 記下原 __setattr__ 的位址
p[6] = id(new_setattr_fn)
a.var = 1                  # see output
p[6] = old_setattr_addr
a.var = 2                  # see output

以上幾個 statement 的輸出應該如下:

new_setattr_fn var 1
aKlass.__setattr__ var 2

沒有留言: