有一天閒着無聊的時候,腦子裏忽然冒出一個Magic Method的有趣用法,能夠用__getattr__
來實現Python版的method_missing
。
順着這個腦洞想下去,我發現Python的Magic Method確實有不少妙用之處。故在此記下幾種有趣(也可能有用的)Magic Method技巧,但願能夠拋磚引玉,打開諸位讀者的腦洞,想出更加奇妙的用法。html
若是對Magic Method的瞭解僅僅停留在知道這個術語和若干個經常使用方法上(如__lt__
,__str__
,__len__
),能夠閱讀下這份教程,看看Magic Method能夠用來作些什麼。python
先從最初的腦洞開始吧。曾幾什麼時候,Ruby社區的人老是誇耀Ruby的強大的元編程能力,其中method_missing
更是不可或缺的特性。經過調用BaseObject
上的method_missing
,Ruby能夠實如今調用不存在的屬性時進行攔截,並動態生成對應的屬性。git
Ruby例子github
# 來自於Ruby文檔: http://ruby-doc.org/core-2.2.0/BasicObject.html#method-i-method_missing class Roman def roman_to_int(str) # ... end def method_missing(methId) str = methId.id2name roman_to_int(str) end end r = Roman.new r.iv #=> 4 r.xxiii #=> 23 r.mm #=> 2000
method_missing
的應用是如此地普遍,以致於只要是成規模的Ruby庫,多多少少都會用到它。像是ActiveRecord
就是靠這一特性去動態生成關聯屬性。編程
其實Python一早就內置了這一功能。Python有一個Magic Method叫__getattr__
,它會在找不到屬性的時候調用,正好跟Ruby的method_missing
是同樣的。
咱們能夠這樣動態添加方法:ruby
class MyClass(object): def __getattr__(self, name): """called only method missing""" if name == 'missed_method': setattr(self, name, lambda : True) return lambda : True myClass = MyClass() print(dir(myClass)) print(myClass.missed_method()) print(dir(myClass))
因而乎,前面的Ruby例子能夠改寫成下面的Python版本:函數
class Roman(object): roman_int_map = { "i": 1, "v": 5, "x": 10, "l": 50, "c":100, "d": 500, "m": 1000 } def roman_to_int(self, s): decimal = 0 for i in range(len(s), 0, -1): if (i == len(s) or self.roman_int_map[s[i-1]] >= self.roman_int_map[s[i]]): decimal += self.roman_int_map[s[i-1]] else: decimal -= self.roman_int_map[s[i-1]] return decimal def __getattr__(self, s): return self.roman_to_int(s) r = Roman() print(r.iv) r.iv #=> 4 r.xxiii #=> 23 r.mm #=> 2000
頗有可能你會以爲這個例子沒有什麼意義,你是對的!其實它就是把方法名當作一個羅馬數字字符串,傳入roman_to_int
而已。不過正如遞歸不單單能用來計算斐波那契數列,__getattr__
的這一特技實際上仍是挺有用的。你能夠用它來進行延時計算,或者方法分派,抑或像基於Ruby的DSL同樣動態地合成方法。這裏有個用__getattr__
實現延時加載的例子。.net
在C++裏面,你能夠重載掉operator ()
,這樣就能夠像調用函數同樣去調用一個類的實例。這樣作的目的在於,把調用過程當中的狀態存儲起來,藉此實現帶狀態的調用。這種實例咱們稱之爲函數對象。code
在Python裏面也有一樣的機制。若是想要存儲的狀態只有一種,你須要的是一個生成器。經過send
來設置存儲的狀態,經過next
來獲取調用的結果。不過若是你須要存儲多個不一樣的狀態,生成器就不夠用了,非得定義一個函數對象不可。htm
Python裏面能夠重載__call__
來實現operator ()
的功能。下面的例子裏面,就是一個存儲有兩個狀態value和called_times的函數對象:
class CallableCounter(object): def __init__(self, initial_value=0, start_times=0): self.value = initial_value self.called_times = start_times def __call__(self): print("Call the object and do something with value %d" % self.value) self.value += 1 self.called_times += 1 def reset(self): self.called_times = 0 cc = CallableCounter(initial_value=5) for i in range(10): cc() print(cc.called_times) cc.reset()
最後請容許我奉上一個大腦洞,僞造一個Dict類。(這個可就沒有什麼實用價值了)
首先肯定下把數據存在哪裏。我打算把數據存儲在類的__dict__
屬性中。因爲__dict__
屬性的值就是一個Dict實例,我只需把調用在FakeDict
上的方法直接轉發給對應的__dict__
的方法。代價是隻能接受字符串類型的鍵。
class FakeDict: def __init__(self, iterable=None, **kwarg): if iterable is not None: if isinstance(iterable, dict): self.__dict__ = iterable else: for i in iterable: self[i] = None self.__dict__.update(kwarg) def __len__(self): """len(self)""" return len(self.__dict__) def __str__(self): """it looks like a dict""" return self.__dict__.__str__() __repr__ = __str__
接下來開始作點實事。Dict最基本的功能是給一個鍵設置值和返回一個鍵對應的值。經過定義__setitem__
和__getitem__
方法,咱們能夠重載掉[]=
和[]
。
def __setitem__(self, k, v): """self[k] = v""" self.__dict__[k] = v def __getitem__(self, k): """self[k]""" return self.__dict__[k]
別忘了del
方法:
def __delitem__(self, k): """del self[k]""" del self.__dict__[k]
Dict的一個經常使用用途是容許咱們迭代裏面全部的鍵。這個能夠經過定義__iter__
實現。
def __iter__(self): """it iterates like a dict""" return iter(self.__dict__)
Dict的另外一個經常使用用途是容許咱們查找一個鍵是否存在。其實只要定義了__iter__
,Python就能判斷if x in y
,不過這個過程當中會遍歷對象的全部值。對於真正的Dict而言,確定不會用這種O(n)的判斷方式。定義了__contains__
以後,Python會優先使用它來判斷if x in y
。
def __contains__(self, k): """key in self""" return k in self.__dict__
接下要實現==
的重載,不但要讓FakeDict和FakeDict之間能夠進行比較,並且要讓FakeDict和正牌的Dict也能進行比較。
def __eq__(self, other): """ implement self == other FakeDict, also implement self == other dict """ if isinstance(other, dict): return self.__dict__ == other return self.__dict__ == other.__dict__
要是繼續實現了__subclass__
和__class__
,那麼咱們的僞Dict就更完備了。這個就交給感興趣的讀者本身動手了。