任務
你需要從某個類或者類型繼承,但是需要對繼承做一些調整。比如,需要選擇性地隱藏某些基類的方法,而繼承并不能做到這一點。
解決方案
繼承是很方便的,但它并不是萬用良藥。比如,它無法讓你隱藏基類的方法或者屬性。而自動托管技術則提供了一種很好的選擇。假設需要把一些對象封起來變成只讀對象從而避免意外修改的情況。那么,除了禁止屬性設置的功能,還需要隱藏修改屬性的方法。下面我們給出一個辦法:
#同時支持2.3和2.4
try:set
except NameError:from sets import Set as set
class ROError(AttributeError):pass
class Readonly:#這里并沒有用繼承,我們會在后面討論其原因mutators = {list:set('''__delitem__ __delslice__ __iadd__ __imul____setitem__ __setslice__ __append extend insert pop remove sort'''.split()),dict:set('''__delitem__ __setitem__ clear pop popitemsetdefault update'''.split()),}def __init__(self,o):object.__setattr__(self,'_o',o)object.__setattr__(self,'_no',self.mutators.get(type(o),()))def __setattr__(self,n,y):raise ROError,"Can't set attr %r on RO obiect" %n def __delattr__(self,n):raise ROError,"Can't del attr %r from Ro object" %n def __getattr__(self,n):if n in self._no:raise ROError,"Can't get attr %r from Ro object" %nreturn getattr(self._o, n)
通過修改 mutators,即 Readonly.mutators[sometype] = the_mutators,還可以輕松地增加其他需要處理的類型。
討論
自動托管是一種強大而通用的技術。在本節的例子中,通過使用這個技術我們能得到和類繼承幾乎完全一樣的效果,同時還能隱藏一些名字。我們在任務中使用這個模擬的子類將一些對象封裝起來,使之變成只讀對象。它的性能也許不如真正的繼承,但另一方面,作為補償,我們獲得了更好的靈活度和更精細的粒度控制。
基本的想法是,我們的類的每個實例都含有我們想要封裝的類型的實例。每當客戶代碼試圖從我們的類的實例中獲取屬性時,除非該屬性已經在類中被定義了(比如定義在 Readonly類的 mutators 字典中),否則__getattr__ 在完成檢查之后,會透明地將這個請求轉交給被封裝的實例。在Python中,方法同樣也是屬性,訪問的方式也一樣,所以無論是訪問方法還是屬性,代碼無須改變。用來訪問屬性的__getattr__方法同時也可用于訪問方法。
解決方案的注釋沒有解釋不使用繼承的原因,這里我們會給出一點解釋。這種基于__getattr__的方式也可用于特殊方法,但僅對舊風格類的實例有效。在新的對象模型中,Python 操作直接通過類的特殊方法來進行,而不是實例的。關于這個問題的更多內容可以在 6.6 節和 20.8節中看到。本節采用的方案——讓 Readonly 類成為舊風格類,從而避開這個問題,并把相關內容留到其他章節——在真實的生產代碼中是不值得推薦的。我在這里用僅僅是為了控制篇幅,同時避免重復其他章節的內容。
setattr__的角色類似于__getattr,當客戶代碼設置實例的屬性時,它就會被調用,這個任務要求某些屬性為只讀,我們只需簡單地禁止屬性訪問操作即可。記住,要在方法的代碼編寫中避免激發對__setattr__的調用,在有__setattr__的類的方法中你不應該使用self.n = v這樣的語句。最簡單的是直接把設置操作委托給類object,如同類Readonly在它的__init__ 方法中所做的那樣。方法__delattr__完成了最后拼圖,它會處理那些試圖從實例中刪除屬性的操作。
以自動托管方式完成的封裝并不適用于采用了類型檢查的客戶代碼或者框架代碼。在那種情況下,客戶代碼或框架代碼完全破壞了多態性,代碼本身應該是被重寫的。記住不要在你自己的代碼中使用類型檢查,因為你可能根本無須那么做。見6.13節提供的更好的選擇。
在 Python 的老版本中,自動托管的流行程度甚至比現在還高,那是因為當時 Python 不支持從內建的類型繼承。而對于現在的 Python,從內建類型繼承是允許的,因此自動托管就用得不那么頻繁了。不過,自動托管仍然具有它的地位——它只是稍微遠離了聚光燈一點點。托管比繼承更加靈活,而有時這種靈活是無價的。除了選擇性地托管(從而高效地實現了某些屬性的“隱藏”),一個對象還可以在不同的時間托管給不同的子對象,或者一次托管給多個子對象,繼承無法提供任何能夠與之相比的特性。下面給出托管給多個特定子對象的例子。假設你有個類,提供各種“轉發方法”,比如:
class Pricing(object):def __init__(self,location,event):self.location = locationself.event = eventdef setlocation(self,location):self.location = locationdef getprice(self):return self.location.getprice()def getquantity(self):return self.location.getquantity()def getdiscount(self):return self.event.getdiscount()and many more such methods
繼承很明顯不適用,因為 Pricing的實例需要托管給特定的location和event實例,這些實例在初始化階段傳入而且可能會被修改。自動托管的補救方法是:
class AutoDelegator(object):delegates = ()do_not_delegate = ()def __getattr__(self,key):if key not in self.do_not_delegate:for d in self.delegates:try:return getattr(d,key)except AttributeError:passraise AttributeError,key
class Pricing(AutoDelegator):def __init__(self,location,event):self.delegates = [location,event]def setlocation(self,location):self.delegates[0] = location
在此例中,我們沒有托管屬性的刪除和設置,而只是托管了屬性的獲取(還有一些非特殊方法)。當然,這個方式只有在我們想要托管的各個對象的方法(以及其他屬性)不會互相干擾的情況下才會充分有效,比如,location最好不要有個getdiscount方法否則它會搶先進行方法的托管,而此方法原本應該是由event對象來執行的。
如果一個需要大量托管的類涉及這種問題,它可以簡單地定義一些對應的方法,這是因為只有在用別的方式無法找到屬性和方法時,__getattr__才會介入。而通過do_not_delegate 屬性還可以隱藏托管對象的一些屬性和方法,而且它也可以被子類改寫。舉個例子,如果類 Pricing 想要隱藏一個叫做 setdiscount 的方法,此方法由 event提供,做一點點修改就可以了:
class Pricing(AutoDelegator):
do_not_delegate = ('set_discount')
其余部分則與前面代碼片段相同。