lazy形容詞,懶惰的,毫無疑問是一個貶義詞。但是,對於計算機領域,lazy卻是非常重要的優化思想:
把任務推遲到必須的時刻,好處是避免重復計算,甚至不計算。本文的目的是拋磚引玉,總結一些編程中的lazy idea,以期有一些啟發。google “lazy”這個單詞,在計算機領域高頻出現三個詞:
lazy loading(惰性加載)、
lazy initializing(惰性初始化)、
lazy evaluation(惰性求值),本文並不刻意區分,因為不管是loading、initializing還是evaluation都需要耗費計算機的運算資源,而且,loading(initializing)也是某種意義上的evaluation。
lazy ideas:
在GOF的設計模式中,並沒有一個叫“lazy loading”之類的設計模式,但是其思想貫穿在很多設計模式中。其中比較明顯的就是singleton和proxy模式。
singleton
單例模式的實現一般都有兩種方式,要么在調用之前就創建好單例對象(
eager way),要么在第一次調用的時候生成單例對象(
lazy way),兩者對象的代碼大致是這樣的:
1 class eager_meta(type): 2 def __init__(clz, name, bases, dic): 3 super(eager_meta, clz).__init__(name, bases, dic) 4 clz._instance = clz() 5 6 class singleton_eager(object): 7 __metaclass__ = eager_meta 8 9 @classmethod 10 def instance(clz): 11 return clz._instance 12 13 14 class singleton_lazy(object): 15 __instance = None 16 @classmethod 17 def instance(clz): 18 if clz.__instance is None: 19 clz.__instance = singleton_lazy() 20 return clz.__instance
PS:在python中,這樣使用單例模式不是很pythonic,更好的辦法可見在stackoverflow上的這篇文章《creating-a-singleton-in-python》。另外在多線程環境下,要實現線程安全的單例還是很復雜的,具體討論可參見iteye上的分析。
proxy:
代理模式屬於責任型模式, 使得一個對象代表另一個對象進行各種操作,常用場景包括
- remote proxy(遠程代理),如RMI, RPC
- virtual proxy(虛代理),根據需要創建開銷很大的對象,如文檔中圖片的加載
- (保護代理):控制對原始對象的訪問, 如智能指針
其中 viatual proxy是使用lazy loading很好的例子
Short-circuit evaluation:
短路求值在絕大多數編程語言都有實現,比較常見的語法如下:
x and y(x && y)
x or y(x || y)
x if bool else y(bool? x : y )
短路求值基本上都是數學邏輯的體現,如果第一個參數已經能推導出這個表達式的意義,那么后面的參數是無需計算的。短路求值非常有用,不僅能避免無用的計算,對於邏輯判斷也非常有用。比如在python中,判斷一個對象的is_ok屬性為True,我們一般這么寫
if(obj.is_ok)
如果obj被賦值成了None,那么就會報一個異常,所以可以寫成
if(obj is not None and obj.is_ok)
python中,一些函數也有短路求值的特性,比如在
這篇文章中提到的any函數:
1 ret = any(self.calc_and_ret(e) for e in elements) 2 def self.calc_and_ret(self, e): 3 # do a lot of calc here which effect self 4 return True(or False)
本意是希望對所有的element都計算,然后返回一個結果,但事實上由於短路求值, 可能后面很多的元素都不會再調用calc_and_ret
generator:
在python和javascript語言中都有generator,generator與普通的函數相比,可以多次(甚至無限次)返回,而且返回值是在需要的時候才生成。在python中,下面兩段代碼非常相似,但事實上差異非常大:
1 for x in [i*i for i in xrange(10000)] 2 # do sth with i 3 4 for x in (i*i for i in xrange(10000)] 5 # do sth with i
generator更廣泛的應用可以參見
《python yield generator 詳解》。javascript中generator的語法和使用與python都非常類似,可以參見
這篇文章。
函數式編程語言中的應用:
lazy evaluation在函數式編程語言中使用得非常頻繁,python也可以當做函數式編程語言來使用,而更為明顯的是
haskell,在其首頁的features介紹里面就有大大的“lazy”
cache:
cache也是一種lazy思想,如果之前有計算結果,那么直接復用之前的結果就行了,干嘛還要重新計算呢?而且最開始的緩存內容, 也是在需要的時候才計算的,而不是一開始就計算好。
wiki上有python實現的簡單例子:
1 class Fruit: 2 def __init__(self, item): 3 self.item = item 4 5 class Fruits: 6 def __init__(self): 7 self.items = {} 8 9 def get_fruit(self, item): 10 if item not in self.items: 11 self.items[item] = Fruit(item) 12 13 return self.items[item] 14 15 if __name__ == '__main__': 16 fruits = Fruits() 17 print(fruits.get_fruit('Apple')) 18 print(fruits.get_fruit('Lime'))
Dirty Flag:
在《
Dirty Flag模式及其應用》一文中,列舉了Dirty Flag模式的諸多應用場景。Dirty Flag顯然也是很明顯的lazy evaluation。比如《
game programming pattern》中的例子:子模型的世界坐標取決於父模型的世界坐標以及子模型在父模型坐標空間的相對坐標,如果父模型的世界坐標變化時就主動去重新計算子模型的坐標,因為兩幀之間父模型的坐標可能多次變換,往往會造成冗余的計算。所以Dirty Flag只是在父模型坐標變化的時候標記,繪制的時候再計划所有受影響的模型的世界坐標。
CopyOnWrite:
CopyOnWrite即寫時復制,如果大家對一份資源只有讀請求時,那么資源是可以共享的,當某個訪問者需要修改資源(寫操作)時,就將資源拷貝一份給該訪問者使用。即資源的拷貝被延遲到了第一次"寫"的時候。CopyOnWrite最廣為人知的兩個應用場景,一個是Unix like系統fork調用產生的子進程共享父進程的地址空間,知道寫操作才會拷貝一份。另一個是java中的copyonwrite容器,用於多線程並發情況下的高效訪問,
cookshell上有對copyonwrite容器的詳細介紹。
web開發中的惰性加載與惰性預加載:
在web前端和APP開發中,當提到惰性加載或者動態加載,大家往往會想到
分頁、輪播圖、瀑布流,這些都體現了惰性加載的思想。其中,瀑布流在出諸多圖片分享網站中使用非常廣泛,比如
花瓣網,當滑動到屏幕底部的時候才會去加載新的內容。為什么要使用惰性加載,第一個是用戶體驗的問題,圖片資源流量比較大,一次加載太多對服務器和瀏覽器壓力都很大,對帶寬要求也很高;另外,可能用戶根本就不會滑動到最下面,多加載的內容就白白浪費了。
當然太”Lazy”了也是不好的,總不能讓玩家滑動到底部才一邊顯示loading icon,一邊開始加載。為了提高用戶體驗,這類網站也會做預加載(predictive loading),即多准備一兩頁的內容,或者在滑屏到一定程度時開始加載新的一頁,不過這樣的預加載也是
惰性預加載(lazy predictive loading)。
總結:
本文列舉了惰性計算在編程中的一些具體例子,希望能給自己以及大家有所啟發,在以后遇到問題的時候多一種解決思路。由於本人編程領域以及編程語言的局限性,肯定還有諸多遺漏,歡迎大家在評論里補充。
references: