標籤:

Python進階課程筆記(三)

4. Bound Method和Unbound Method

聲明: 本系列文章中的所有內容都是基於Python 2.x版本的,原因是網易絕大部分項目都是在用2.x版本,筆者參與過的項目無論端游還是手游都是基於Python 2.7.x版本進行的開發,因此無論經驗還是課程適用性,都是在2.x範圍內。在Python 3.x中,unbound method的概念已經被取消了。

上一小節說了,Bound Method和Unbound Method這部分是我參與大雄的課程中最喜歡的一部分,因為它讓我窺探到了Python語言動態特性的一角,也加深了我對於平時在用的一種優化方法的認識。這部分會比較長,我們分幾個小節來細說。

4.1 基本概念

先從代碼來看,定義一個簡單的類A:

class A(object):n def foo(self):n passnna = A()n

很簡單,我們來列印一些信息來看看:

print A.foo # <unbound method A.foo>nprint a.foo # <bound method A.foo of <__main__.A object at 0x023DE070>>nprint A.foo == a.foo # Falsennprint a, id(a) #<__main__.A object at 0x0235E070> 37085296nprint A.foo.im_self # Nonenprint a.foo.im_self # <__main__.A object at 0x0241E070>n

為了方便對比和理解我把輸出的結果放在了對應的print之後,首先輸出兩個A.foo和a.foo,看到了他們分別是兩個對象,一個叫做unbound method,一個叫做bound method,很明顯他們是兩個不同的對象。這跟我從C++角度來理解Python的方法(Method)就有點不同了——通常靜態語言中,面向對象的設計,把方法的定義放在類上,對象通過一定的機制(比如虛函數表等)查找到對應的函數地址來進行調用,那按這樣推理,A.foo和a.foo應該是一個東西才對。事實證明Python語言不是這樣的。

繼續看後面的輸出代碼,我們列印了方法的im_self屬性來進行查看,發現A.foo的該屬性為None,a.foo.im_self的該屬性為a。

那麼概念上的答案就很明顯了,unbound method和bound method的區別在於是否綁定了im_self屬性。更加準確的是Python官方文檔的描述:

While they could have been implemented that way, the actual C implementation of PyMethod_Type in Objects/classobject.c is a single object with two different representations depending on whether the im_self field is set or is NULL (the C equivalent of None).

如果你直接調用A.foo()這種未綁定的方法,就會有這樣的提示:

TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)n

而且可以通過A.foo(a)這種方式來進行手動的綁定,這也是早期Python的一種常用調用方式。到這裡可能還有很多疑問,在對基本概念進行明確之後,我們繼續往下看。

思考:對象實例a的foo屬性存在在哪裡?

4.2 動態創建

我們來做一些分析工作:

print 1, id(a.foo) # 1 37674480nprint 2, id(a.foo) # 2 37674480n# print a.__dict__ # {}n

多執行幾次a.foo來獲取函數,發現他們的id是一樣的,上面執行都是38002160,這很正常,他們看上去就應當是同一個對象。我們嘗試列印a的__dict__屬性來看,結果是空的,也就是說foo這個對象不存在在a對象身上,那它存在在哪兒呢?對應的類A身上?我們剛才列印的a.foo和A.foo並不是同一個對象啊。

好,然後我們來做一點奇怪的事情:

print 1, id(a.foo) # 1 37674480nprint 2, id(a.foo) # 2 37674480nm1 = a.foonm2 = a.foonprint 3, id(m1) # 3 37674480nprint 4, id(m2) # 4 37594296n

什...什麼鬼?!

我使用兩個變數,都獲取a.foo這個bound method對象,然後列印他們的id,結果m1和m2的id不同,意味著他們兩個不是同一個Python對象

思考:對象身上的方法對象,分別賦值給兩個變數,結果發現它們不是同一個對象,但是之前直接執行a.foo來進行id的輸出,他們的id又是一致的,這是什麼神奇的邏輯?

好,在揭曉答案之前,我們再看一些分析代碼:

print 1, id(a.foo) # 1 32824816nprint 2, id(a.foo) # 2 32824816nnm1 = a.foonm2 = a.foonprint 3, id(m1) # 3 32824816nprint 4, id(m2) # 4 32744632nnprint 5, id(a.foo) # 5 32824896nnm2 = Nonenm1 = Nonenprint 6, id(a.foo) # 6 32824816n

由於多次執行,id值跟上次執行的不在相同,我們只在本次執行的代碼輸出中進行對比。在輸出m1和m2的id之後,我們再次輸出a.foo的id,發現它改變了。=_=,好吧,我已經見怪不怪了,當我刪除了m2和m1兩個變數之後,再次查看id( a.foo ),它又變回了原來的值32824816。。。

如果你還沒有看出什麼端倪,那可能會有種被逼瘋了的感覺。

但是,通過這個id的值來看,有沒有中似乎什麼對象被複用了的感覺?

Python是有小對象緩存池機制的,對於int、float,string甚至tuple、list都會有不同的對象緩存機制,那這裡a.foo返回的對象是被緩存過的,才會出現重複的id。

本來在印象中,應當是一個對象就可以搞定的事情,為什麼有多個對象,還要有緩存池?那是因為——

Bound & unbound methods are almost temporary instance objects.

是的,無論bound還是unbound method,幾乎都是臨時的實例對象。

m1 = A.foonm2 = A.foonnprint 7, id(m1) # 7 36625904nprint 8, id(m2) # 8 36545720n

它們這些對象在通過.進行屬性訪問的時候創建,這裡有個疑問我暫時沒有查到,這一創建過程是在__getattr__方法中還是在__getattribute__方法中?暫時沒有查到,需要再去看下源碼。

動態創建,加上緩存池的應用,共同造成了上述的id變化的現象。由於id(a.foo)這行代碼在執行完畢之後,對於當時創建的對象的引用就已經為0了,因此會被立即釋放,Python出於性能的考慮,把這個對象放入了池中,再次執行id(a.foo)的時候,不再重新創建對象,而是從緩存池中去拿,因此id是一樣的,當使用m1這樣的屬性引用住了對象的時候,再次調用a.foo,就會創建一個新的對象,因此id就不同了。當del m1之後,釋放的對象又重新放回到緩存池,等待被複用。

思考: 如果我把m1、m2兩個變數的釋放順序反過來,然後再去取id,會有有不同?

我們通過代碼來看一下:

print 1, id(a.foo) # 2 37281264nprint 2, id(a.foo) # 2 37281264nnm1 = a.foonm2 = a.foonprint 3, id(m1) # 3 37281264nprint 4, id(m2) # 4 37201080nnprint 5, id(a.foo) # 5 32824896nnm1 = Nonenm2 = Nonenprint 6, id(a.foo) # 6 37201080n

解釋這個現象很容易——緩存池是使用棧的數據結構來存儲的,按照對象歸還順序入棧,獲取的時候從棧頂拿,那麼後面獲取的對象就是棧頂最新歸還的對象。

4.3 foo在哪裡

Bound method和Unbound method對象都是在調用的時刻創建,然後引用計數為0的時候被釋放,這解釋了他們從哪兒來到哪兒去的問題,但是還沒有解釋這些方法執行的時候調用的代碼在哪裡。

前面提到了一個點,就是a身上的__dict__里並沒有foo變數,但是也可以調用到foo方法,這是Python的屬性獲取機制來決定,首先從self的__dict__中檢索,如果沒找到會從type(self)的__dict__中檢索,這裡就是A,可以看下A的__dict__屬性:

print A.__dict__ n# {__dict__: <attribute __dict__ of A objects>, __module__: __main__, foo: <function foo at 0x024CA4B0>, __weakref__: <attribute __weakref__ of A objects>, __doc__: None}n

這裡有個key為foo的對象,注意,它是一個function對象,而不是unbound method對象,這也證明了它的動態生成。那a.foo這個bound method對象是如何訪問到這個function對象的呢?我們來看一下:

print A.foo.im_func, a.foo.im_func # <function foo at 0x0254A4B0> <function foo at 0x0254A4B0>nprint A.foo.im_class, a.foo.im_class # <class __main__.A> <class __main__.A>n

無論是bound method還是unbound method,都是有im_func和im_class這兩個屬性的,其中im_func屬性就是A.__dict__中的foo對象,im_class就是A對象,這樣就不難猜測a.foo調用過程中的過程,最終是通過im_class和im_func來找到真正執行的代碼。

4.4 Python語言的動態特性

思考: 說了這麼多,理解了原理,那麼Python語言為什麼要這麼設計呢?

這麼設計其實是為了實現Python的動態特性,我們來看一個例子:

class A(object):n def foo(self):n return Anndef foo(self):n return Bnna = A()nnprint foo, A.foo.im_func, a.foo.im_func # <function foo at 0x0230A2B0> <function foo at 0x0239A4B0> <function foo at 0x0239A4B0>nA.foo = foonprint foo, A.foo.im_func, a.foo.im_func # <function foo at 0x0230A2B0> <function foo at 0x0230A2B0> <function foo at 0x0230A2B0>nprint a.foo() # Bnprint A.foo(a) # Bn

這段代碼其實是遊戲中常用的hotfix的一種實現原來的demo。

所謂hotfix,是指在玩家不知情的情況下,替換掉客戶端腳本代碼中的部分邏輯,實現修復bug的目的。

對於靜態語言,進行這種運行時修改代碼比較麻煩,而且像ios這樣的平台禁止了在數據段執行代碼,也就是你無法動態替換dll,使用native的語言或者C#的幾乎不能方便地進行hotfix,這也是腳本語言在遊戲行業里(尤其國內)面非常常用的原因。

上述例子中,A.foo = foo這句代碼替換了A的__dict__中的foo對象,由於方法對象都是在使用時動態生成的,因此無論是新創建的對象還是已經在內存中存在的對象,都會在調用方法的時候重新生成方法對象,它們的im_func屬性就指向了類中的新的屬性對象。

4.5 代價

動態的代價,就是慢。

C++靜態語言的方法調用,即使考慮繼承的情況,也不過是一次虛表的查詢操作,而python中不但有各種__dict__檢索,而且要通過__mro__屬性向繼承的父類進行搜索,這塊具體的過程在後面進行分析。然後加上對象的創建過程,影響效率可想而知。

因此,我們在代碼中常用的一種優化是:

如果在一段代碼中有對於對象屬性的頻繁訪問,在不會修改其內容的前提下,通常會使用一個局部變數保存屬性的應用供後面的代碼邏輯使用。

代碼中通常會有a.b.c.d.func()這樣的調用,如果這段邏輯在一個循環中,就可以先定義一個func = a.b.c.d.func,然後通過func()來進行函數調用即可。

性能差別有多大?我們使用Python的timeit來做一些測試:

import timeitnnclass A(object):n def __init__(self):n self.value = 0nn def foo(self):n passnna = A()nnn = 100000000nprint timeit.Timer(a.foo, from __main__ import a).timeit(n)nm = a.foonprint timeit.Timer(m, from __main__ import m).timeit(n)n

輸出結果

6.63377350532n1.8554838526n

在執行一億次的情況下,是6.6s和1.8s的差距。注意這裡並沒有真正執行foo函數,而只是獲取這個屬性。其實這裡我有一個疑問,是屬性訪問導致的這麼大的性能差異,還是bound method對象的生成呢?於是我又添加了對於value屬性的訪問測試:

value = a.valuennprint timeit.Timer(a.value, from __main__ import a).timeit(n)nm = a.foonprint timeit.Timer(value, from __main__ import value).timeit(n)n

輸出結果:

4.79706941201n1.85150998879n

可以看到,單純的屬性訪問也是有很大的性能差異的,但是即使在有緩存池的情況下,同樣通過.來訪問屬性與訪問方法,也有較大的性能差異,這就是Python為了實現動態特性所付出的代價之一。

總結:從一個語言概念,探究到語言的實現,再到把這個語言特性應用到工程中,這需要技術的積累和積澱,而透過初看難以理解的現象分析出問題的本質,需要更多耐心和經驗。

2016年7月2日晚於杭州家中

推薦閱讀:

如何去尋找網路爬蟲的需求?
[23] Python模塊和引入
[18] Python元組
Python Web 框架大亂斗:哪個框架適合你?
Python學到什麼樣子可以去找工作?

TAG:Python |