關於Python Magic Method的若干腦洞

有一天閒着無聊的時候,腦子裏忽然冒出一個Magic Method的有趣用法,能夠用__getattr__來實現Python版的method_missing
順着這個腦洞想下去,我發現Python的Magic Method確實有不少妙用之處。故在此記下幾種有趣(也可能有用的)Magic Method技巧,但願能夠拋磚引玉,打開諸位讀者的腦洞,想出更加奇妙的用法。html

若是對Magic Method的瞭解僅僅停留在知道這個術語和若干個經常使用方法上(如__lt____str____len__),能夠閱讀下這份教程,看看Magic Method能夠用來作些什麼。python

Python method_missing

先從最初的腦洞開始吧。曾幾什麼時候,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__屬性的值就是一個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就更完備了。這個就交給感興趣的讀者本身動手了。

相關文章
相關標籤/搜索