2009年11月8日 星期日

我學 TDD 的方式

學 TDD ( 測試驅動開發 ) 的動機請見《為什麼要寫 unit test?為什麼要先寫測試?》。這篇簡記一下我學習 TDD 的方式:

看完書後,大致上是從大師的 Blog 跳著看有興趣(或著說,讀起來比較有感覺)的部份。自己寫個一陣子就看個幾篇,待思考消化後再回頭試著用自己的方式寫出來。如此反覆進行。最重要的是在自己的環境裡,用自己的方式進行 TDD,感受會更明確。

TDD 的概念很簡單,看過例子後應該能馬上使用,但無法立即精通 TDD 。它是一個習慣,而培養習慣需要時間累積。養成習慣後,接著需要提昇測試和重構的技巧,才能逐步提昇效果並降低使用 TDD 的成本。如同過去學 Design Pattern 或其它寫程式的技巧一般,需要讀書和時間練習。我在前幾次使用 TDD 時有犯一些錯,使得效果打折,有感受到一些好處,但也對一些壞處感到疑惑。直到寫了
五、六個小專案後,才釐清一些疑慮,確信 TDD 是很划算的取捨,從而決定持續使用 TDD。最近用它寫超過一萬行程式,更加感受到它的威力。

備註

  1. Java 的例子可見 The Bowling Game Kata;Python 的例子可見 Dive into Python 3 介紹 unittest module 的章節和介紹 refactoring 的章節。《Dive into Python 3》的例子比較不像真實的 TDD,為了教學方便,作者直接寫最終版的測試碼。若要看原汁原味的演化過程,看 Java 的例子或 Kent Beck 的書較適當。

2009年10月29日 星期四

賀!Python 寫破萬行記念!

這是我獨力寫過最大的專案了,從八月初開始寫,不知不覺寫到破萬行啊,而且其中有四千多行是測試碼,貫徹當初 TDD 到底的決心。

不得不說,幸虧有用 TDD,不然測試碼大概會丟三落四的,愈後面會愈痛苦,最後就會想砍掉重練。開發過程裡遇過幾次很難改的情況(第一次發生在寫到兩千多行時),幸好仗著測試碼夠齊全,將相關程式重構後,不知不覺就把新功能寫好了。

有時沒啥想法,不知怎麼改較好,就想說「總之就先重構,船到橋頭自然直」。結果這招矇對的機率還滿高的,有時還會發覺比原本構想更簡單的做法。有些人認為重構成本很高,且短期沒有產值,似乎不適合常做。事實上,在測試碼充沛且時常重構的情況下,可以三兩下完成重構,再加上版本管理系統的輔助,重構失敗立即重來,十分方便。

使用 TDD 偶而會有意外收獲,發覺巧妙設計。有時即使看個數次,還是學不會自己完成目標用上的新技巧,感覺好像用外掛過關,卻不知道自己怎麼過的。最後只能期望下回遇到類似的處理,TDD 仍能導引出良好的設計。對照《物件導向程式的九個體操練習》來看,發現在 TDD 的導引之下,自然地會做出較佳的設計(因為較差的設計不好測),而且會因地制宜,做出貼近需求的做法,不會 overdesign。

雖說在很久以前就確定 TDD 是正確的路,也領悟出可測性 (testability) 是首要之務,為了可測性而改變原本的設計也是合適的決定。還是需要許多實際經驗,才能更充份地體會這些原則帶來的影響。我喜歡 freedom 說過的一句話「 system design 一直都是 trade-off」,經過這一年多的歷練 [1],我更肯定 TDD 帶來的 trade-off 是值得的,加強寫測試碼的技術絕對是值回票價的投資。

咦?這篇好像離題成行銷 TDD,回頭來補講 Python 的心得。寫這個專案的途中接觸到比較深的 Python 議題,像是用 ctypes 包 C/C++ API 很方便;好用的畫圖函式庫 matplotlib;還有一拖拉庫的速度問題讓我學了不少,比方說:

  • cProfile 絕對是找瓶頸的頭號幫手,做 profiling 時也不會拖慢多少。除演算法的考量外,別自己亂猜浪費時間。
  • list 很大但不需用到 sublist 或 index 的功能時,可以用 generator 改寫。
  • 若有個欄位被存取數百萬次以上,用 data field 會比用 method 快上不少,因為 Python 的 function call 不快。
  • collections.defaultdict 比 dict.get(key, default_value) 快上一些。
  • CPython 使用 GIL 造成 multi-thread 比 single-thread 還慢,要想用多 CPU 加速,可以用 2.6 內建的 multiprocessing。multiprocessing 用法不難,可是有一堆小細節會爆炸,要仔細讀官方文件。

雖說調速度很辛苦,整體來看還是值得的,而且 Python 和 C/C++ 之間溝通很容易,針對效能改寫的成本不高,更何況有測試碼撐著,修改很容易。讓我覺得用 Python 快速開發原型,針對瓶頸用 C/C++ 改寫或平行化的流程還挺不錯的。

除了以上這堆拉里拉雜的收獲,更大的收獲是學到了一個專案的演化過程。過去自己太心急,什麼都想一開始做到最好,擔心後期會愈來愈難改,災難一發不可收拾。結果是搞得自己很累,開發效率很差、程式品質漸漸變糟也不知道從何改善。後來放開心胸,延伸 TDD 的精神,先做最主要的部份,再慢慢修補小問題:像是註解、模組文件格式、行寛該定多少 [2]、某設計會不會太慢、會不會太髒等。稍微轉換思維、改變自己的習慣,會發現過去許多困擾都不見了,似乎多明白了一點「擁抱變化」的含意了 [3]。

備註

  1. 接觸 TDD 的概念;用它實作近十個小專案包含 C++、Java、Python 三種語言;單一專案寫了上萬行的程式。
  2. 我後來習慣用 85。這樣用 24″ 螢幕配 putty 預設字型大小,用 VIM 剛好可以垂直分割成兩個視窗而不會折行。
  3. 話說我以前一直覺得這個詞很嘴炮啊。

2009年9月22日 星期二

寫 Python 測試碼的好幫手

前陣子用 TDD 寫了個六千多行的工具,這篇記錄一下過程中惠我良多的好幫手。

unittest

內建模組。

用法見官網介紹或《Dive into Python 3》的第九章 Unit Testing ( 有附帶介紹 TDD ) ,如同 Java 的 JUnit 3.x,用 unittest.TestCase 來寫 test case 很方便。

coverage

安裝方式:easy_install coverage。

Code coverage 是分析測試碼品質的方法,標示出測試碼沒有執行到那幾行程式碼。使用 Code coverage 的基本精神是:Code coverage 的數據高不表示測試碼有效,但數據低的話,測試品質必定不好。至於高低要如何界定?這得看專案的類型,比方科學計算型的程式,要求 80% 以上並不過份;而有一堆圖形介面的程式,可能連到 60% 都很難。

coverage 官網有簡單易懂的使用例子。由於 Python 的功能限制,coverage 無法作到像 Java 那麼全面。coverage 的限制以及 Code coverage 的注意事項,詳見作者的說明:“Coverage testing, the good and the bad.”

nose

安裝方式: easy_install nose。

寫 unittest 時,管理 test suit 是件很瑣碎又易犯錯的事,相信很多人會想說,能不能跑個程式,自行搜集目錄下全部的測試碼並自動執行。沒錯,大家的心聲 nose 聽到了!這裡直接用例子說明 nose 的使用方式:

  • 執行目前目錄下所有測試:
    1
    
    nosetests
  • 執行目前目錄下所有測試並附上子目錄 pkg1、pkg2 的 Code coverage 資訊:
    1
    
    nosetests --with-coverage --cover-package=pkg1,pkg2 --cover-erase
  • 不要執行 slow_test.py:
    1
    
    nosetests -e slow_test.py
  • 使用四個 CPU 平行執行測試:
    1
    
    nosetests --processes=4

–with-coverage 需要先裝 coverage;–process 得另裝 package multiprocessing ( easy_install multiprocessing ),相關說明詳見 Multiprocess: parallel testing。

另外,若要讓 nose 跳過物件 A 的測試,就在程式裡寫上

1
A.__test__ = False

比方若不想測模組 mod,就在 mod.py 裡寫上

1
__test__ = False

sqlite3

內建模組。

用法見官網介紹。在測試資料庫時,個人覺得 local database 比 mock 好用,方便準備資料,測起來也比較踏實,而且使用 memory mode 可大幅減少執行時間。附帶一提,sqlite 跨 C、Python、Java 等語言,支援 SQL 92,執行速度又快,相當好用。

PyMox

安裝方式: 用 SVN 從官網 checkout 原始碼,再用 setup.py 安裝:

1
2
3
svn checkout http://pymox.googlecode.com/svn/trunk/ pymox-read-only
cd pymox-read-only/
sudo python setup.py install

用法見官網文件。PyMox 是 Google 開發的 Python 版 EasyMock,我試用過 Java 的 EasyMock 後覺得用法挺直覺的,就決定用它,如此一來學一套工具可以同時用在 Java 和 Python 上。使用 mock 的好處是簡化測試碼,更完備地測試程式,像是代換掉處理資料庫、網路連線的物件。如此一來,連測試「讀資料到一半卻斷網」的應對情形,都是輕而易舉的事。若想立即體會 mock 的功效,不妨配合 mock 用 top-down 的方式作 TDD,會發覺不同的程式開發思維。

2012-08-05 更新: 我現在改用 Mockito 了,更為簡單易用 。

2009年9月20日 星期日

Honey & Clover I, II 觀後感

Honey & Clover
又看了一次 Honey & Clover 第一部和第二部,已經明白劇情發展的我,這次還是一樣被情節給牽動著,忍不住一集集地看下去,而且更能注意細節,更能感受到作者細膩的鋪陳。

看完許久,仍然無法用文筆表達情緒的我,只好照老樣子看著別人的心得,和著劇中插曲讓感覺漸漸褪去。最後的結局,究竟小育是抱著什麼樣的心情,將那份點心交給竹本的呢?

小育想傳達的心意,竹本確實地收到了。一再地重覆看著這段,我不知道自己感受到的是什麼,劇中插曲的歌詞是這麼寫著的:

平靜清澈的溪水,毫不躊躇地向前流淌。
令人懷念的午後熏風,吹拂著濕透的脖頸。
野兔不停奔跑的樣子,白百合的耀眼花姿,
夜空中閃耀的星群,都理所當然地在我眼中。

相信一定會觸碰到的幻影,
充斥著言語的底片中的街道延續著。
再見吧,再見吧,向著不知何時的你。
如果還能遇見你就好了。

只能不斷地聽著插曲,試著沉浸在兩人的心情中,隨手寫出片段的思緒。

小育和竹本彼此的心意,不是友情、愛情之類的字眼可以表示的,那究竟是什麼呢?對於他們而言,這樣的分別又代表什麼呢?為什麼如此的不捨?為什麼如此地感傷?滿腹疑問的我似乎明白答案為何,卻無法述諸言語,或是不願述諸言語。於是不斷重覆聽著插曲、重覆想著兩人的心情、隨手敲著鍵盤,讓這份情感深刻地注入內心深處。對於祝福的人以及被祝福的人都能幸福,感到無比地欣慰,只是感傷也隨著揮之不去。

2009年9月6日 星期日

康特的難題

書籍基本資料:《康特的難題》。

看到中後半時,由於相當在意某位角色出奇的應對方式,讓我不得不硬衝了兩小時看到結尾,導致我破例到淩晨兩點才睡,上回這樣挑燈夜戰讀小說,應該是國高中時看金庸小說吧?

若以一般小說來說,算是普普吧。但這本揭開科學界的生活,並用非科學界人士質疑科學界視為常識的文化,還有點出女性科學界的困境,都很有意思,可滿足我對科學界人性的好奇心。像是學界慣於使用的「我們」,究竟意指為何?作者順序的含意?提出想法和執行實驗何者較為重要?實驗室成員是合作者,還是手下?害怕被盜用想法而不敢發表,以及急於發表以追求第一發現者榮譽的衝突?實驗數據不如預期,做些「適當」的調整,是修正誤差還是照假?女性學者在追求終身職和小孩之間的抉擇?書中點出許多與科學誠實、無私、公正、理性、分享等特質矛盾的人性弱點,有許多令人深省的地方。角色的佈局也滿有意思的,將幾位看似無關的核心角色,慢慢地交錯在一起,若起頭和結尾能更圓潤就更棒了。

看完這本書後,讓我更不想走學界路線,我明白自己很難抗拒種種誘惑堅持自己。即使能堅持自己,也很難要求合作者照自己的要求進行,有太多灰色地帶,很難令合作者與自己達成共識。還是遠離利益得失,才能自在地以自己的步調前進。

2009年9月5日 星期六

驗證第一、想法第二

看到這篇《The Only Truly Failed Project》,其中有兩段話寫得很棒:

Failure is a wonderful teacher. But there’s no need to seek out failure. It will find you.

The only truly failed project is the one where you didn’t learn anything along the way.

最近觀察不少數據和實驗結果,推翻之前許多猜想,不過我沒有像碩士做研究時那般痛苦,反而有些興奮。大概是因為沒有發論文的壓力吧,加上先前讀過費曼的言論,覺得這是理所當然的情況,我們知道得本來就很少,想法通常也是錯的。然而,重點在於如何從錯誤的嘗試中學習?

昨天晚上不斷思索這堆錯誤的嘗試告訴我什麼,仍未有定論。忽然想到愛迪生的話,當他實驗千種材料失敗後,他說,至少我明白這千種材料不能作為燈炮的材料。
於是心裡又更踏實了。但這不表示可以隨意的嘗試,在沒有有效的驗證方式之前,無謂的嘗試無法提供任何訊息。

印象中費曼提過,他不和人討論無法驗證的想法,那是沒有意義的行為。提出猜想後,我會思考如何驗證。直到有辦法進行驗證前,我會先保留想法,改試別的作法。或是將原問題先拆成幾個小問題,讓小問題能夠被驗證。待各個擊破搜集到一些資訊後,再回頭看是否能驗證原本的想法。

能夠驗證的想法,才能從中學習。不然結果出來後,我們無從判斷結果有多接近目標,自然也無法從中學習,進而做修正。舉例來說,如果要做 AI 下棋的搜尋,要怎麼知道目前的策略有效?和人下棋是個辦法,可是沒有無法頻覆進行,也就不能確保方法的可靠程度。另一方面,光看評估盤面函式的輸出值也無用,沒法確認分數高確實是有利的盤面。

換個想法,提兩個策略,互相對戰。至少可以確定每次都贏的策略,是相對來說較好的策略。一個策略可以用 greedy algorithm,另一個用自己想的特別方式。雖然我們預期特別的方法會贏,但若結果相反,也能從中明白新東西:像是 greedy algorithm 比想像中的有效,可從中找到好點子;或是特別的方法不如預期地有效,也就不用再和人下棋,減少後續測試的成本。若擔心兩個方法自動對戰變化有限,可以引用不同棋譜的中盤局面,再引入兩個方法擔任不同角色,觀察好的方法是否在各種相同局面裡,無論擔任那一方,都能下得較好。

再舉解魔術方塊的例子。為了能夠驗證,可以先將解好的盤面一步步弄亂,記下正確的還原步驟和所需步數。準備好多組測試盤面(即最後弄亂的樣子),測試演算法解的效果,步數和預期步數相差多少?那一步發展變得不同?於是有確實的數據可以分析,明白問題出在那。有未知的大進步時(只花了預期的一半步數),也方便觀察出原因(如弄亂盤面時做了不當的重覆操作),不會不小心高估方法的成效。

看起來理所當然的事,沒想到我這麼遲才明白,驗證是如此的重要。而這個觀點卻是從 coding 那邊先萌芽,才接著在研究這邊確實實行。不論是研究還是coding,我認為重要性是: 需求 (動機) > 驗證 > 想法 (解法) > 實作。如《管理是什麼》書裡提到,管理即為客戶創造價值 -- 相當含糊的定義,可是卻非常精闢。不論是研究還是寫程式,也要先確保能滿足某種需求,之後的發展才有意義。接著,在天馬行空地想解法前,先確保有方法驗證方法的好壞、達到目標的程度、有辦法分析結果,之後才能確實地落實想法。於是,即使失敗仍能有所收獲。

2009-09-06 Update

經 LCamel 一提,發現英文的用詞很有趣,Verification and Validation 裡這麼解釋:

It is sometimes said that validation can be expressed by the query “Are you building the right thing?” and verification by “Are you building the thing right?” “Building the right thing” refers back to the user’s needs, while “building it right” checks that the specifications be correctly implemented by the system.

意即:

  • 滿足需求 = validation = do the right thing.
  • 驗證作法 = verification = do the thing right.

語言真是奇妙啊,用一句話來總結,就是「Are the do you right thing right?」

2009年8月10日 星期一

《別鬧了,費曼先生》閱讀中的雜記

讀《別鬧了,費曼先生》的過程裡,偶而有寫片段感想。這篇是這些短記整理後的記錄。

通用的學習法則

《別鬧了,費曼先生》提到費曼常跨領域學些不同東西,不管是哲學、數學、生物學,費曼都用他學習科學的習慣判別對錯,掌握基本原理。最近做研究也有這種體會。即使沒有背景知識,有良好邏輯,掌握假設和觀察結果,通常能做出正確的判斷。前提是我們能明確判讀那些是客觀事實,那些是主觀判斷,數據不會騙人,可是人會,而且我們自己也可能會騙自己。

舉例來說,看到別人的實驗結果和實驗報告,一定要自己讀一遍數據,不然無法判斷別人說明中那些部份為主觀推測。像是兩個方法有效程度差了千分之一,是否可說有顯著差異?去除人有私心的因素外,我們也沒有想像中的聰明,容易將一些相似的資訊混在一起,不知不覺認定是事實。「誤把有相關性視為有因果關係」是常見的例子,有興趣的人可以看些寫給大眾看的統計書籍。反用「判讀個人觀點和事實」的原則在自己身上也很有效,可以找到自己思路的盲點,不會過於武斷主張自己的看法。

此外,舉例說明是最佳的討論方式。費曼提到他和數學家討論數學原理時,他的判斷方式是心中想像一些實例,對方描述問題時,他就想像心裡那些物體會怎麼變化,於是當對方說某某性質如何,他發覺在他想像世界裡有矛盾時,就能做出辯駁。另一方面,我相信只有當我們能舉例說明時,我們才算真的明白,藉由逼自己舉例,可以思考的更靈活,找出忽略的細節。

別死記名詞

另一方面,費曼也指出許多人記了一堆名詞,卻沒掌握基本精神,他在 MIT 的大學同學如此,愛因斯坦的研究助理也會犯這種錯。費曼一再提到很多學生只會背名詞,不會理解背後的含意,不明白他們在學什麼。這種精神從他小時候就存在,和父親在林中散步時,費曼的父親向他解釋生物行為而不背名字,費曼將自己的成就歸功於父親童年時的教導,不無道理。

費曼在問學生問題時,會舉例討論,而不問教科書式的問題。我在確認別人能力時也是這麼做,不問專有名詞解釋(像是請說明 X 演算法為何),盡量用應用題直接看對方怎麼運用知識,這樣能立即明白對方有沒有理解背後含意。反過來看,不知道某些名詞不代表什麼,就算對方不知道 Dijkstra’s Algorithm,但若能提出相似的解法,反而更高明。只要和對方說明這些知識,他們立即能掌握精神加以運用。反之,知道 Dijkstra’s Algorithm 卻無法活用,知道更多演算法、累積更多經驗,成長也有限。

名詞只是方便溝通的工具,若不能理解背後的含意,光記許多名詞毫無意義。在資訊爆炸的時代,每天產生一堆新的名詞,吸收這堆雜亂的詞彙時,得時時提醒自己,是否明白背後含意。以前我曾落入背誦知識為樂的陷阱,後來發現知道很多皮毛卻無法做進一步運用,才驚覺自己浪費不少時間。舉例來說,現在當紅的 Android,即使我知道 Android 是 Google 發佈的手機平台,也沒任何幫助,只是一堆名詞堆砌。到不如了解為何會有 Android?適合解什麼樣的問題?需要時可視需求對相關知識做進一步研究。 (備註:我並不了解這兩個問題,只是舉例而已。)

並不是說記名詞毫無意義,費曼童年時自己發明一套數學符號,他覺得讀起來比較好理解,很高興地用了一陣子。但後來發現他無法和人溝通,就把這些符號丟了,改用共通符號。與人溝通需要專有名詞,不然難以進入問題核心,講半天還在說「你是說如果有 n 個點,點可以是任意東西,像是城市,或想像成座標也行,這 n 個點之中部份兩點有互相連接,連接的距離長短不一,而你想從某個起點 A 走到終點 B,想問如何走才能花最少的步數嗎?」(即最短路徑問題)。