最近在看《松本行弘的程序世界》,其中講到Ruby和Javascript里的原型編程,覺得非常靈活。我記得之前看Python教程的時候也有提到過類似的機制,無奈當時不求甚解,現(xiàn)在只好補補課。
在原型編程中,對象的成員變量和方法都可以動態(tài)修改,通過復制已有的對象來實現(xiàn)代碼的重用。這樣不需要事先定義好的類就可以實現(xiàn)面向?qū)ο缶幊?。要實現(xiàn)原型編程,首要條件就是對象的成員變量和方法可以動態(tài)修改。Python提供了setattr()和delattr()兩個函數(shù)來動態(tài)修改對象的屬性,也就是成員變量和方法。先看下面一段代碼:
class Foo(object):
def do(self):
print("done")
o = Foo()
try:
print(o.bar)
except:
print("no attribute bar found")
setattr(o, "bar", 1)
try:
print(o.bar)
except:
print("no attribute bar found")
delattr(o, "bar")
try:
print(o.bar)
except:
print("no attribute bar found")
第一次print(o.bar)的時候,因為Foo類型的對象沒有這個屬性,所以會拋出異常;第二次打印的時候,已經(jīng)通過setattr()添加了bar屬性,所以可以成功;之后又用delattr()刪除了屬性,所以第三次打印又是異常。事實上,還有更簡單的寫法:
o.bar = 1 # 等價于setattr(o, "bar", 1)
del o.bar # 等價與delattr(o, "bar")
在Python里,每個自定義類的對象都有一個__dict__成員變量,這個字典里記錄了這個對象的動態(tài)屬性(在類定義之外添加的屬性)。對于Foo類型的對象o,打印出來就是這樣:
>>> o = Foo()
>>> o.bar = 1
>>> o.__dict__
{'bar': 1}
動態(tài)添加的bar在__dict__中,而類中定義的do不在。__dict__在對象外部是可讀可寫的,相當于一個public類型的成員變量。通過修改對象的__dict__,就可以修改對象的屬性。所以前面的代碼還有第三種寫法:
o.__dict__["bar"] = 1 # 等價于setattr(o, "bar", 1)
del o.__dict__["bar"] # 等價與delattr(o, "bar")
Python的內(nèi)建類型,比如int、str或者object是沒有這個成員變量的,因此這些類型的對象是不能動態(tài)添加屬性的。既然可以對象的屬性可以動態(tài)增加,那么把一個函數(shù)賦值給o的一個成員變量,是不是就成了一個方法了呢?
def func():
print("hello")
o.say = func
o.say()
看起來好像問題已經(jīng)解決了,不過其實沒有那么簡單。在類的成員方法中,第一個參數(shù)都是self(比如前面的do方法),通過它可以訪問對象的成員變量,相當于C++的this。但是這里的func()函數(shù)并沒有self參數(shù),因此也沒有辦法引用成員變量。我們試試給func()添加一個self參數(shù):
def func(self):
print("hello")
再把它設置為o的屬性:
>>> o.say = func
>>> o.say()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 1 argument (0 given)
這次直接拋出異常,告訴我們func()函數(shù)缺少一個參數(shù)。而對于正常的成員方法,調(diào)用的時候是不需要顯式的傳遞self參數(shù)的,這說明func()函數(shù)距離真正成為成員函數(shù)還差了那么一丟丟。用type()函數(shù)查看一下類型,可以發(fā)現(xiàn)do和say原來是不同的類型:
>>> type(o.do)
<type 'instancemethod'>
>>> type(o.say)
<type 'function'>
看起來只要把func轉(zhuǎn)換instancemethod類型就可以了。Python的types包中提供了MethodType來完成這種轉(zhuǎn)換[1]:
>>> from types import MethodType
>>> o.say = MethodType(func, o, Foo)
>>> o.say()
hello
到這里我們的目的已經(jīng)達到了。不過我還想補充一點兒關(guān)于開放類的內(nèi)容。《松本行弘的程序世界》第6章中講解了Ruby的開放類。在Ruby中,已經(jīng)聲明的類可以在代碼中動態(tài)的修改,可以添加和刪除方法,或者給方法起別名。這種方法很有用,比如RoR里就通過AcitveSupport對Ruby的基本類型進行了擴展,所以可以像下面這樣寫代碼:
2.weeks.ago
Python中其實也有類似的機制。在Python里,一切皆對象。所謂的類,也不過是一種特殊類型的對象罷了。先看看上面的Foo類,其實也有__dict__屬性:
>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__module__': 'test', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
不過這個__dict__的類型有點不一樣,不是字典,而是dict_proxy,這個我們暫時不討論,只要知道這里的__dict__是不能修改的就好了。
>>> Foo.__dict__['a'] = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dictproxy' object does not support item assignment
雖然這種方式不能用了,但是setattr()和Foo.bar = 1這兩種寫法還是可以的,因此類的屬性也是可以動態(tài)增加的。動態(tài)增加Foo類的屬性,是否會影響已經(jīng)生成的Foo類對象呢?
>>> o = Foo()
>>> Foo.bar = 1
>>> o.bar
1
>>> Foo.bar = 2
>>> o.bar
2
答案是肯定的。反過來,修改對象o的屬性,是否會影響到Foo呢?接著前面的代碼繼續(xù)執(zhí)行:
>>> o.bar = 3
>>> Foo.bar
2
>>> Foo.bar = 4
>>> o.bar
修改對象o的bar屬性不會影響類Foo的bar屬性,并且當o的bar屬性被修改之后,再次修改Foo.bar,就不會對o.bar產(chǎn)生影響了。這有點類似與copy-on-write機制。在沒有修改o.bar的時候,對象o其實是共享了Foo類的bar屬性,因此修改Foo.bar,o.bar也隨之改變。一旦修改了o.bar之后,就創(chuàng)建了一個bar屬性的副本,再修改Foo.bar就不會影響o.bar了。其實只要把整個過程中對象o的__dict__屬性打印出來就很清楚了:
>>> Foo.bar = 2
>>> o.__dict__
{}
>>> o.bar = 3
>>> o.__dict__
{'bar': 3}
>>> Foo.bar = 4
>>> o.__dict__
{'bar': 3}
給Foo類增加bar屬性的時候,o.__dict__是空。這是打印o.bar,由于o本身沒有bar屬性,就會找到Foo類的bar屬性。而對o.bar進行了修改之后,o.__dict__里就多了一個key為“bar”的項目,也就是給o本身增加了一個bar屬性,此后再打印o.bar,訪問的就是這個屬性,而不是Foo.bar了。
前面給類添加了成員變量,要添加方法也是一樣的,先用types.MethodType()將函數(shù)轉(zhuǎn)換成instancemethod類型,然后復制給某個屬性。給類添加方法時,MethodType()的第二個參數(shù)可以填None,不指定任何對象。
總結(jié)一下,Python還是很靈活的,不過美中不足的是,前面講的東西僅僅適用于自定義類型。對于Python的內(nèi)建類型,比如int、str和object,不論是類型本身還是對象,都是不能動態(tài)修改的。所以Python也沒有辦法像前面RoR那么靈活的去編程。另外,這種靈活性其實是雙刃劍,濫用的話會讓代碼變得很難讀,因為讀者找不到某些類或者對象的屬性都是在哪里添加的,也就很難理解代碼。
-
這個方法是在https://segmentfault.com/q/1010000004087006看到的,里面還提供另一種使用partial的實現(xiàn)方式,這里暫不討論。 ?