1. 程式人生 > >讀懂花裡胡哨的14行Python程式碼!

讀懂花裡胡哨的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, 保證在一次呼叫後,將結果快取起來。若目標方法只是一個不耗計算資源的簡單方法,就沒必要再使用了。