最初在Python官網的tutorial文件中看到lambda function時,雖然文件中有幾個使用範例,也在網路上看了一些相關的教學文章,但實在是看不出來有什麼非要用這玩意兒的理由。或許就如官網文件中所說的,lambda function就只是syntactic sugar而已,所以也就沒特別在意,直到在設計Game of Life的輸入介面時,因為需要用到,兜兜轉轉,費了好些功夫和時間,總算對它的用途和用法有比較完整的認識。
在Game of Life的輸入介面中,最重要也最不容易處理的部分,應該是怎麼樣可以讓人很容易輸入原始的universe樣態。既然universe是棋盤狀的方格,而每個方格,也就是cell的狀態,不是生,就是死,那最好的方式,就是可以用滑鼠點選方格來改變狀態。要想做到這樣的功能,在Python官網文件提到的module中,turtle和tkinter,是看起來比較像是含有所需功能的兩個module。
turtle主要是用來繪圖,同時也提供了處理滑鼠事件及鍵盤事件所需的工具。工具看來很齊全,不過要拿來處理universe的輸入問題,可得花費一番功夫。首先,要先畫出棋盤狀的方格。然後當使用者點選某個方格時,要透過滑鼠事件偵測滑鼠游標的位置,並計算這個位置是在哪個方格,接著改變這方格裡面所有像素的顏色。當然啦,最後還必須紀錄這個方格的狀態。唉呦!實在是太繁雜了,想想都覺得煩,不想寫。
古聖先賢總是教訓我們:做人要甘願、要勤勞,不可以偷懶!可是說實在的,要是太認命,面對工作就一頭栽進去埋頭苦幹,從不多動動腦子,想想是不是有什麼辦法可以簡化工作、提昇效率,現在哪來那麼進步的科技?不說別的,先前寫「核心計算」部分,在判斷cell是不是位於universe邊界的時候,要不是看著一堆if不順眼,同時也覺得,要考慮那麼多不同的邊界狀況,實在是有夠麻煩的,又怎會去想辦法設計出更乾淨俐落又有效的方法來?所以啊,要多想想是不是有那種可以偷懶,但卻有效的辦法。
既然不想用turtle做,那用tkinter行不行?tkinter主要是用來設計GUI,所以像是按鈕、標籤、視窗、捲軸、選單等各種常見的元件,一應俱全。不過挺討厭的是,有些元件在不同的程式語言中,有不同的名稱,所以明明知道元件的長相,還得費一番功夫找出對應的名稱。唉!就不能大家都叫一樣的名稱嗎?鐵鎚就長那樣,你叫它榔頭,它還是長那樣;checkbox就長那樣,你叫它checkbutton,它還是長那樣,那幹嘛弄個不同的名稱作弄人?很煩咧!不過煩歸煩,還是要想想有沒有哪個元件合用。
在tkinter中,每個元件的長相、功能都是固定的,能夠調整的東西就那些,所以現在主要的問題是:universe是棋盤狀的方格,用滑鼠點選方格時,方格的狀態以及顏色會改變。有哪些元件可以做出這樣的效果?先前在考慮用turtle做時,是把universe看成是一大塊,然後在裡頭畫出小方格。很顯然的,對於tkinter的元件來說,這樣子的做法行不通,得想想是不是有其他辦法。
既然在一大塊裡頭畫出小方格的方式行不通,那用一堆小方格組成一大塊行不行?在寫「核心計算」部分時,就曾經想過類似的問題。那時候在想:是要把universe看成是點陣圖,圖裡頭的pixel就代表cell,還是要把所有代表cell的方格看成是一個一個獨立的物件,由這些獨立的物件拼湊組成universe?這兩種方式都可以達到目的,差別只在於程式好不好寫,還有處理的效率。後來之所以採用點陣圖的方式,是覺得這樣會比較好寫。那現在如果把方格看成是物件,透過滑鼠的點選可以改變顏色,還能同時處理資料,有沒有哪個tkinter的元件具備這樣的功能?嘿嘿嘿!那個網路上到處都有的「按鈕」,不就是個這樣子的存在嗎?!
tkinter的按鈕元件語法中,當按鈕被按下時,要觸發的事件是透過command這個parameter來指定,寫法是:command = callback_function,也就是說,當按鈕按下時,會去執行callback_function。這看起來挺單純的,網路上許多教學文章都能看到說明。不過啊不過,網路上絕大多數的教學文章,都是一個按鈕配上一個callback_function,所以顯得很單純。但是現在是要用一大堆的按鈕來組成universe,這可不能聽從古聖先賢做人要甘願、要勤勞、不可以偷懶的教訓,真的埋頭苦幹,一個一個按鈕、一個一個callback_function的寫一大堆。那要怎麼寫?當然是用迴圈加上list來寫囉!而lambda function也就在這兒展現它的存在感。
雖然lambda function在這裡展現了它的存在感,不過這過程挺不乾脆的,遮遮掩掩、躲躲藏藏,頗費了一番功夫和時間才撥雲見日,把它給看個通透。
一開始還不知道要用lambda function,所以程式寫成這樣:
def change_state(btn):
btn['bg'] = 'red' if btn['bg']=='white' else 'white'
for i in range(10):
btn[i] = tk.Button(root_window, bg='white', command=change_state(btn[i]))
這段程式的目的,是產生10個白色的按鈕,當滑鼠點選按鈕時,那個被點選按鈕的顏色,就會由白色變為紅色,或由紅色變為白色。只是這樣子的寫法是錯的,在change_state中,會出現錯誤訊息:
TypeError: 'int' object is not subscriptable
想想也對,這btn[i]都還沒生出來,就把它當argument傳給change_state,如果這樣也可以,那就太神奇了。既然如此,那就等btn[i]生出來後,再來處理command,程式的迴圈部分就改成:
for i in range(10):
btn[i] = tk.Button(root_window, bg='white')
btn[i].configure(command=change_state(btn[i]))
改成這樣之後,錯誤訊息是消除了,不過生出來的所有按鈕,都是紅色的,而且不管怎麼點,顏色都不會變。
這到底是怎麼回事呢?實在是摸不著頭緒,後來想起曾在教學文章中有提到,可以用lambda function把callback_function的程式碼直接寫在command之後,而不用另外寫在def中,於是就把configure那行程式改成:
btn[i].configure(command=lambda: change_state(btn[i]))
天不從人願,這樣寫也不行。按鈕是生出來了,而且點選時顏色也會切換,可是不管點選哪個按鈕,切換顏色的,永遠是最後那個按鈕。
會不會是因為lambda function裡頭,呼叫change_state時,是用for迴圈的index來直接指定要改變狀態的是哪個按鈕,所以造成這樣的結果?改用parameter的寫法不知道行不行得通?試試看就知道!把lambda function改成這樣:
lambda n: change_state(btn[n])
結果不改還至少有個按鈕顏色會切換,改了之後,所有的按鈕都是白色的,不管怎麼按,就是不會變成紅色的。
唉!沒輒了,上網找答案。
原來,要這樣寫才對:
lambda n=i: change_state(btn[n])
這什麼語法啊,n=i?還有啊,那幾個錯誤的寫法,到底問題在哪?這樣子的疑問想要上網找解答,還真是考驗搜尋的功力。搜尋看似簡單,但要是不知道要用什麼關鍵字去找,那還真像是在荒郊野外找路一樣,充滿了天地茫茫,何方是歸處的感覺。
找了很久,試了很多關鍵字,每隔幾天想到新的方向、新的關鍵字就找一找,總算在stack overflow的討論串裡頭,看到了個比較清楚的解釋。好玩的是,那個討論串是在討論lambda function到底有什麼用,而不是在討論tkinter的按鈕寫法。難怪先前用tkinter相關的關鍵字搜尋,會找不到比較清楚完整的解釋,方向不對啊!
正確的寫法有了,那先前錯誤的寫法,到底錯在哪了?簡單來說,問題就出在對lambda function的運作方式不夠瞭解。
程式寫成lambda: change_state(btn[i])時,因為i是for迴圈的index,是在lambda function外面,是global variable,所以只有在lambda function被呼叫時,才會去存取i。在迴圈裡頭的程式碼,雖然造出了按鈕,但也就僅止於這樣,沒有去執行其他的什麼東西。而lambda function的部分,也就只是定義而已,要等到按鈕被按下,才會去執行。迴圈執行完建造按鈕的工作後,i的值是9,所以當按鈕被按下時,呼叫lambda function執行的,就是change_state(btn[9])。難怪不管按哪個按鈕,都是最後一個改變顏色。如果把lambda function改成lambda: change_state(btn[i-1]),那不管按哪個按鈕,都是倒數第二個改變顏色,可以進一步驗證這個解釋。
那後來把程式改成lambda n: change_state(btn[n])時,問題又出在哪?原來,因為n是lambda function的parameter,所以當按下按鈕時,n還是n,它一直就是n,不會變成argument。既然如此,不管按鈕怎麼按,根本就沒有任何argument傳給change_state,它只好把你的呼叫動作當空氣,已讀不回。
因為在定義lambda function時,並不會去存取global variable,所以在建造按鈕時,必須使用local variable來讓傳入change_state的按鈕,與當時迴圈index所指定的,是一致的。就因為是這樣,所以才會寫成lambda n=i: change_state(btn[n])。
呼!總算搞懂是怎麼回事了!可是,那個n=i又是怎麼回事?網路上找到的討論,都是直接就這麼寫、這麼用,沒有解釋。這個問題放在心中好一陣子,直到有天晚上心血來潮,想說在官網的文件找找看,說不定可以找到。
哈!還真的找到了!以lambda function為目標來找,在官網文件中的一些說明,不但解答了這個疑惑,連先前花了好多時間才在網路上找到解答的疑惑,都有很清楚的說明。這些文件包括:
- Programming FAQ: Why do lambdas defined in a loop with different values all return the same result?
- The Python Turorial: 4.8.6. Lambda Expressions
- The Python Language Reference: 6.14. Lambdas
n=i這個語法的說明,就躲在第三個文件裡頭,而且還偽裝得挺好的,乍看之下沒察覺,直到後來才發現。怎麼說呢?文件裡頭是這麼寫的:
lambda_expr ::= "lambda" [parameter_list] ":" expression
然後在最後一段裡頭提到,那個parameter lists的語法,就參考8.7. Function definitions裡頭的說明。看到那段話,突然間醒悟過來:n=i不就是呼叫function時,keyward argument的寫法嗎?都說是lambda “function”了,在呼叫時,argument的寫法,不就該和function一樣?!真是的,怎麼就一直沒想到咧?!
其實那幾份文件先前都看過,只是那時就只是為了要知道Python有哪些東西而看,並不是為了尋求疑問的解答而看,所以就有如走馬看花,有看沒有到。為了解答心中的疑惑而看文件,就好像打開了主動雷達,原本被動接收,看來平凡無奇不是那麼重要的內容,就變成字字珠璣,充滿了深意。想想這尋覓瞭解lambda function的過程,還真是「衆裏尋他千百度,驀然回首……」像極了愛情啊~~~
後記:準備結束這篇lambda function的隨筆時,想到還有個疑問還沒解決,就是寫成btn[i].configure(command=change_state(btn[i]))時,為什麼生出來的所有按鈕,都是紅色的,而且不管怎麼點,顏色都不會變?上網查了一下,原因是因為在command=change_state(btn[i])中,change_state(btn[i])是keyward argument,所以實際上指定給command的,是執行change_state(btn[i])之後的傳回值。在建造按鈕時,會執行一次change_state。而因為change_state並沒有任何實際上的傳回值,它傳回的是None,所以最後得到的結果是command=None。這也就是為什麼所有的按鈕都是紅色的,而且點選時沒任何反應的原因。