Monday, April 30, 2007

Generator與串接Asynchronous calls

我們時常必須以一定的順序呼叫底層module所提供的函式。比如說,
def callSeq(api):
    api.setEnv()
    api.produce()
    api.report()
這在任何procedural language當中都是很直覺的事情。但是GUI app的開發原則是,UI thread必須能夠很快的消耗event queue,並且把繁重的工作交給其他thread執行。因此,我們時常會期望API所提供的內容是asynchronous的,如此一來GUI的反應才會順暢。如此一來,我們就必須「等待」每一個async. call的結果,才能夠進行下一個動作。想像上,我們可以這麼做:
def createBatch():
    jobs = workerthread.todoQueue
    #jobs is a thread-safe queue
    jobs.push(api.setEnv)
    jobs.push(api.produce)
    jobs.push(api.report)
接著,設計workerthread,讓他從todoQueue當中不斷找事情做。缺點是,我們仍然必須要在workerthread當中處理api的各種callback,或是event,才能在一個task結束之後,繼續下一個task。同時,我們也必須對queue進行各種操作與管理。這時候generator便可以幫上忙。
def thingsTodo():
    yield api.setEnv()
    yield api.produce()
    yield api.report()

todo = thingsTodo()

#here we assume that the "task completion"
#is passed to an unified callback
def asyncCallback(event):
    try:
        if(event == 'done'):
            todo.next()
    except StopIteration:
        print("all jobs done")

api.setAsyncCallback(asyncCallback)

#then we invoke the first thing to do
todo.next()
我們相當於是利用了generator來幫我們進行簿記的動作。值得注意的是,todo.next()必須在逸出current scope之後仍然有效。這也就是closure的觀念…在local function之中,使用了參數、local變數,以及他們的衍生結果,而這個local function可以被外部的object所使用,並且在local scope結束之後,仍然有效。 這樣的作法,很簡單就可以定義出calling sequence。事實上,我們可以把thingsTodo()真正要做的,想成「在這個function的這裡先暫停一下,之後再回來繼續」的概念(yield時return,但是記住狀態。下次的function entry point移從yield敘述句之後)。這樣的概念叫做稱為continuation。我們只是用generator來模擬這樣的效果。由於Python的generator有著不少限制,所以目前並不算是直接在語言層級支援continuation的語言。 題外話,如果api提供的介面是像這樣:
api.setEnv( callbackWhenDone )
api.produce( callbackWhenDone )
api.report( callbackWhenDone )
那麼我們可以這樣把呼叫串連起來
def callDone(event):
    print "done"
def callReport(event):
    api.report(callDone)
def callProduce(event):
    api.produce(callReport)
api.setEnv( callProduce )
當然,callback一般都需要判斷回傳event為成功或是失敗,所以實際狀況會更為繁瑣。無論如何,closure與local function,這兩種可以動態決定function definition的功能,讓我們免於使用額外的資料結構處理關於api calling sequence的維護,可以讓彼此有關係的code segment距離接近,進而提升程式的可讀性。

Friday, April 27, 2007

Python的Generator

Python的generator,說起來就是用起來像是iterator的function。由於撰寫一段function,並讓他攜帶某些資訊,要比重新打造一個支援特定iteration模式的container要簡短許多,generator可以在許多方面降低程式的複雜度,甚至提昇效率。 首先我們以尋訪檔案資料夾為例。假設我們現在要遞迴的掃瞄一個資料夾,並且對每一張圖片製作縮圖,我們可以這樣寫:
def findAndGenThumb(root):
  for item in os.listdir(root):
     if isPicture(item):
       genThumb(item)
     if os.path.isdir(item):
       findAndGenThumb(item)
相當直接明瞭。但是,如果之後要對每一張圖片抽取其EXIF資訊呢?為了程式的結構簡單,我們必須要分離「尋訪資料夾的每一個圖片元素」與「對每一個圖片元素施加某種操作」兩個意圖。
def findPic(root):
    files = []
    for item in os.listdir(root):
        if isPicture(item):
            files.append(item)
        if os.path.isdir(item):
            files.append(findPic(item))
    return files
這樣一來,我們先呼叫findPic('c:\\')取得一個含有圖片名稱的list,接下來便可以對該list進行操作:
for p in findPic('c:\\'):
    genThumb(p)
    #...Anything operates on a picture file
這個作法的問題在於,檔案數量可能非常龐大,而存在一個list裡面會消耗相當程度的空間。難道我們不能夠想辦法「修改回圈的內部」嗎?generator便提供了這樣的機會。 Generator的便是在function內使用關鍵字yield。我們可以把yield想成return,只是該function碰到yield並且回傳值之後,並不會結束,而是暫時凍結自己的執行狀態。同時,對於外面來說,這個function第一次呼叫的結果,會是一個iterator。之後每一次呼叫iteroator.next(),就會得到該function所yield的值。值得注意的是,generator一次只送回一個iteration(一次yield)執行的結果,並沒有暫存的container。而在每次呼叫next()之間,該function的狀態會被儲存起來。 跟一般的container比較一下:
container = [1,2,3,4,5] 
def tempStorage(c):
    tempList = []
    for item in c:
        print item
        tempList.append(item)
    return tempList

for item in tempStorage(container):
    print "receive" + str(item)
會得到 >>>
1
2
3
4
5
receive1
receive2
receive3
receive4
receive5
而換成一個generator:
def genStorage(c):
    for item in c:
        print item
        yield item

#use this generator as a container
for item in genStorage([1,2,3,4,5]):
    print "receive" + str(item)
會得到
1
receive1
2
receive2
3
receive3
4
receive4
5
receive5
也就是說,Generator每次執行到yield就會停住,下一次next()被呼叫的時候才會再「醒過來」。最後,我們用generator來尋訪檔案樹,把他轉換成一個container,但是不需要把所有檔案都記起來。如果檔案數量很多,generator的版本會省很多記憶體,因為被交換的只有yield後面的物件,而沒有一個很大的container:
#A generator that walk through file tree
def getPics(root):
    for item in os.listdir(root):
        if isPicture(item):
            yield item
        if isDir(item):
            getPics(item)

#Use that generator as a iteratable container directly
for p in getPics('c:\\')
   doSomething(p)

#Or call it's next explicitly
g = getPics('c:\\')
while True:
    try:
        pic = g.next()
        doSomething(p)
    except StopIteration:
        #g.next() raises "StopIteration" if the actual generating function exits.
        break
這樣一來,是不是很簡單易懂呢?而且,就算檔案很多,也不用花費多餘的記憶體。而使用起來就像是一般的container一樣直覺、單純、一致。不需要自己寫一個支援iteration的class,也不需要用stack或是queue來簿記目前尋訪到哪一層。聽起來是不是很熟悉呢?是的,我們可以把檔案樹想成一個資料結構,而這個資料結構需要某種特定的尋訪方式。透過generator,我們可以制定各種想要的尋訪方式,並且把結果轉換成為另一個container。 Generator方便之處在於會幫你記住function裡面local scope的執行環境,不用再自己使用一些global的狀態變數,也不會像C/C++的function static variable一樣,要時常注意重複呼叫的問題,同時也免除自己製作一個class進行繁瑣的狀態維護動作。 除了拿來做traversal,generator也可以拿來產生合成的collection,或是產生無限大的collection,或是用於分攤昂貴的loop成本。由於Application layer勢必要處理、過濾各式各樣的collection,generator的好處在這些地方就很明顯了。