讀懂花裡胡哨的14行Python程式碼!
最近在專案裡發現一段程式碼,初看比較難看懂,細看,也還是比較難看懂。遂研究了一下,證實了這段程式碼確實,沒啥作用,遂刪之。記錄在下。
去掉幾個用於封裝的函式,保留最小程式碼後,剩下以下14行程式碼。你能說出這段程式碼是幹嘛的?每行程式碼分別被執行了幾次嗎?
class CachedProperty(): def __init__(self,func,name=None): self.func = func self.name = name or func.__name__ def __get__(self,instance,cls=None): res = instance.__dict__[self.name] = self.func(instance) return res class MyClass(): @CachedProperty def caches(self): return {} mc = MyClass() mc.caches['key'] = 1 print(mc.caches['key'])
要讀懂上面14行程式碼,需要理解兩樣東西,一個是類裝飾器,一個是描述器。
類裝飾器
裝飾器比較常見,類裝飾器就不常見了。
給func1加上func2裝飾器,等同於令func1=func2(func1), func1依然是一個callable的物件。給func1加上class2裝飾器,同樣需要返回的是一個callable的物件。所以在class2中,要求實現__call__方法。
進群:960410445 即可獲取數十套PDF!
例子:
class log(): def __init__(self,func): self.func = func def __call__(self, *args, **kwargs): print('log...') return self.func(*args, **kwargs) @Log def func1(x): return x
以上就等同於令func1=Log(func1), 實際上就是一個callable的Log類例項了。呼叫func1的時候,就觸發__call__方法完成呼叫過程。
描述器
描述器是一個有"繫結行為"的物件屬性,用其他程式語言做一個較通俗的對比,就是實現了setter和getter的屬性。
定義一個描述器,需要為屬性至少定義__delete__, __get__, __set__中的任意一個方法。這些方法會在進行屬性訪問時自動被呼叫。
一個簡單的例子:
class Ds(): def __init__(self, v): self.v = v def __set__(self, obj, v): self.v = v def __get__(self, obj, objtype): return self.v class T(): x = Ds(10) print(T.x) # 10
這裡還要扯到一個優先順序的問題, 如果只定義了__get__方法,那麼當T.__dict__擁有同名屬性的時候,優先使用T.__dict__的,若還定義了__set__方法,則描述器永遠優先,稱為非資料描述器,反之稱為資料描述器。這一點對讀懂14行程式碼至關重要。
程式碼解釋
回到最初的14行程式碼,可以說是'使用類裝飾器的方式來定義一個非資料描述器,利用優先順序的特性,完成方法呼叫結果快取的功能'。
一行行來看
class CachedProperty(): def __init__(self,func,name=None): # func == caches self.func = func # self.name == 'caches' self.name = name or func.__name__ # instance等於物件例項, 在本例子中,等同於mc,cls等同於MyClass def __get__(self,instance,cls=None): # 最右邊返回{},賦值給中間的mc.__dict__['caches']和最左邊的res res = instance.__dict__[self.name] = self.func(instance) return res class MyClass(): # 類裝飾器, 和下面三行一起,等同於 caches = CachedProperty(lambda: {}) @CachedProperty def caches(self): # 不需要被當成方法呼叫, 所以不需要實現__call__ return {} mc = MyClass() # 第一步,獲得mc.caches, 按優先順序, mc.__dict__['caches']不存在, # 所以觸發__get__方法呼叫,得到{}, 且mc.__dict__['caches']也等於{} # 第二步,為mc.__dict__['caches']['key']賦值1 mc.caches['key'] = 1 # 按優先順序,mc.__dict__['caches']已存在,不再觸發__get__方法和cahces(self)方法執行 # 取mc.__dict__['caches']['key'], 返回1。 print(mc.caches['key'])
以上,經過一通花裡胡哨的操作,實際效用就是: 實現了caches(self)方法只會被執行一次,其結果將被快取起來,供以後再次呼叫時使用。
在這個例子中,caches(self)只有一行程式碼,返回{},在簡化前,也是大概這個意思,所以,在本專案中,這段程式碼實際上毫無作用,遂可刪之。
之後,發現同樣的做法出現在django和flask等框架中,我的理解是,CacheProperty目的是為了實現view等較耗資源函式的結果快取,並做到懶載入。例如,某類方法需要載入一個大模板並進行渲染,就可以使用CacheProperty, 保證在一次呼叫後,將結果快取起來。若目標方法只是一個不耗計算資源的簡單方法,就沒必要再使用了。