顯示具有 Python 標籤的文章。 顯示所有文章
顯示具有 Python 標籤的文章。 顯示所有文章

2011年8月7日 星期日

Python 的特別之處 (1)

從新手的眼中來看 Python,比較能看出 Python 和其它語言不同之處。最近有機會幫別人快速上手 Python,就順便整理一下我從中發覺 Python 較為突出的優點。

list、dictionary and string

平時 coding 最常用到的 container 就是 list 和 dictionary,另外也會常用到字串操作,Python 提供方便的方法來操作它們。string 可看成一個有實作 list 介面的類別,一些常用操作像是 slice:"abcd"[1:3] 回傳 "bc";負數的索引: "abcd"[-1] 回傳 "d";直接和 for-loop 整合在一起:

In [1]: for ch in "abcd":
  ....:         print ch
  ....:
a
b
c
d

讓存取這些常用資料型態輕鬆許多。

iterator

使用 iterator 比傳統的 for (i=0; i<n; i++) 來得清楚,Python 針對 iterator 下了不少工夫,提供好用的輔助函式,像是 enumerate 補足需要用到 index 的情況:

In [2]: for i, n in enumerate([1, 3, 5]):
  ....:     print i, n
  ....:
0 1
1 3
2 5

使用 zip 整合多個 list:

In [3]: names = ["John", "Marry", "Tom"]
In [4]: sexes = ["Male", "Female", "Male"]
In [5]: for name, sex in zip(names, sexes):
  ....:     print name, sex
  ....:
John Male
Marry Female
Tom Male

map, filter and reduce

任何使用過 map 的人,都會喜歡 map 輕巧的用法,來看幾個例子:

In [1]: map(int, ["12", "37", "999"])
Out[1]: [12, 37, 999]
In [2]: map(str, [12, 37, 999])
Out[2]: ['12', '37', '999']

int 是一個函式,將傳入的物件轉成整數;str 則是轉成字串。使用 map 可以將一個 iterator 轉為另一種 list。

另一個常見的情境是,從一個 list 裡篩選出需要的物件,比方說只留下偶數:

In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: filter(lambda x: x % 2 == 0, numbers)
Out[2]: [2, 4]

或像 filter(lambda s: s.endswith('.py'), file_names) 只留下結尾為 ".py" 的字串。

除 map 和 filter 的重心放在轉換 list 之外,reduce 則是將 list 匯整成一個物件。有了這些函式,就能任意的操作 list,用以匯整或擴散資料容器。

比方說將一串數字加起來:

In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: reduce(lambda x, y: x + y, numbers, 0)
Out[2]: 15

上面這個例子可以用內建的 sum 取代,來看另一個複雜點的例子,將一串 0、1 值合成一個整數:

In [1]: bits = [0, 1, 0, 0, 1]  # bits[i] 的值表示 2^i 的系數

In [2]: reduce(lambda x, (i, b): x | (b << i), enumerate(bits), 0)
Out[2]: 18

list comprehension

map 和 filter 雖然方便,要用到 lambda 或是混合使用時就沒那麼好讀了。Python 提供一個重量級的武器 list comprehension 來解決這問題。比方說留下偶數並乘以三再加一:

In [1]: numbers = [1, 2, 3, 4, 5]

In [2]: [n * 3 + 1 for n in numbers if n % 2 == 0]
Out[2]: [7, 13]

綜合以上的語法,可以輕鬆地寫出易懂的 quick sort

def qsort(numbers):
    if len(numbers) <= 1:
        return numbers                                                                                     
    pivot = numbers[0]
    rest = numbers[1:]
    smaller = [n for n in rest if n <= pivot]
    larger = [n for n in rest if n > pivot]
    return qsort(smaller) + [pivot] + qsort(larger)

對於習慣 C、C++、Java 世界的人來說,應該不曾看過這麼直覺易懂的 quick sort 吧。

tuple

tuple 是一個很妙的資料結構,它和 list 的主要差別是它是唯讀的,Python 裡鮮少有這種唯讀物件。不過它較易發覺的好處是被用在 Python 的 parallel assignment 和函式傳回值。

於是在 Python 裡可以這麼寫:

a, b = b, a # swap

Python 在看到 b, a 時會產生一個 tuple 表示 (b, a),再透過 tuple 達到 parallel assignment

函式也可以一次「傳回多個結果」:

In [1]: def divide_and_mode(a, b):
   ...:     if b == 0:
   ...:         return None, None
   ...:     return a / b, a % b
   ...:

In [2]: divide_and_mode(7, 3)
Out[2]: (2, 1)

In [3]: a, b = divide_and_mode(7, 3)

In [4]: a
Out[4]: 2

In [5]: b
Out[5]: 1

原理一樣是先轉成 tuple 再傳回,再視等號左側放什麼,決定要存成 tuple 或做 parallel assignment

2012-01-25 更新

應該沒什麼力氣更新續篇,在這裡簡短描述一下,有興趣的人可以找看看相關介紹。

with

在 Python 2.6 後,支援用 with 管理資源。像讀檔案可以用 with 的方式寫:

# 印出所有使用者的 id
with open('/etc/passwd') as fr:
    for line in fr:
        print line.split(':')[0]  

在進入 with 的 block 前,會呼叫 file object 的 __enter__ 以獲得 file descriptor;在離開 block 前會呼叫 __exit__ 關掉 file descriptor。即使中間呼叫了 sys.exit() 或丟出 exception,仍會執行到 __exit__,不用擔心會漏關。方便用在許多情境 (比方說 lock / unlock、自動 flush output buffer),易讀易用。

內建常用函式庫

除上述的基本資料結構和 string 外,還有 sqlitejson等。

簡單不易出錯的語法

舉幾個寫 C 可能發生的問題,但在 Python 的語法下則不會發生:

if (condition);
{
    // BUG!! 這裡的程式一定會被執行
}
if (x < 60)
    number_of_fail++;
    total_fail_score += x; // BUG!! 這行每次都會執行

另外,由於 Python 的 condition 只能是 expression,不能是 assignment。不會有 if (x -= 3 < 0) 這種 bug。

ipython

有 ipython 方便快速試語法、試函式庫還有開發程式。ipython 比簡單的 interactive interpreter 強大許多。

2010年11月4日 星期四

讓 if、else 帶有更明確的語意

最近在維護程式時對於 if、else 有更深的體會,一但邏輯分支變多,很難釐清各種控制流程,一些簡單的習慣可以大幅簡化除錯和改程式的負擔。

變數的初始值

一個常見的情境是有個變數會依條件而有不同的值,典型的寫法如下:

1
2
3
4
5
# 假設後面有一長串算式會乘上 weight,這裡先決定它的值
if double:
   weight = 2
else:
   weight = 1

( 備註,在 Python 裡 if / else 裡設的變數和它的外層是同一個 scope。 )

或是善用程式語言提供的三元運算子設值 (即 ? : ),在 Python 裡則是這麼寫:

1
weight = 2 if double else 1

若有多種情況,在其它語言裡可能會用 switch,我個人不喜歡 switch,覺得用起來不直覺,Python 裡也沒有 switch,但可以用 dict 代替:

1
2
3
4
weight = {
    'double': 2,
    'triple': 3,
}.get(condition, 1)

操作複雜時可在 dict 的 value 裡改用輔助的小函式,明確的用簡短的程式表明「這區塊在決定 weight 的值」。

別小看這一點小改變,當程式碼很多時,看到 “value = a if condition else b” 可以立即明白這裡的判斷式是用來設值,可以省下為 if、else 這區塊煩心的時間,也可以減少消耗精神和腦內暫存記憶。

提前處理簡單的分支

以用遞迴的方式實作費氏數列為例:

1
2
3
4
5
6
def fib(n)if n < 0:  # Error input.
        raise ValueError('n must be positive.')
    if n == 0 or n == 1return 1
    return fib(n - 1) + fib(n - 2)

上面的寫法先處理例外,接著就能放心處理正常的情況,再來處理特例 (初始值),最後就能專心和主邏輯奮戰,而覺得主邏輯變得單純許多,很好處理。

較大的程式,就是先寫幾個簡單輔助小函式 (例如 is_invalid()),先呼叫小函式避開特殊情況,一樣可以化繁為簡。

避免巢狀區塊和 continue、break

常見到在多層迴圈裡呼叫 if、else,並和 continue、break 混用,我個人覺得這種寫法很亂,而傾向用小函式 + return 避開使用 continue 或 break,比方像下面的程式要從一個兩層 list 裡找出每個 list 第一個負數,並算出負數的總和:

1
2
3
4
5
6
7
sum = 0
for numbers in a_list_of_numbers:
    for n in numbers:
        if n < 0break
    if n < 0sum += n

可以改用小函式配合 return 避免使用 break 並「隱藏」分支:

1
2
3
4
5
6
7
8
9
10
def find_first_negative(numbers)'''Return 0 if there is no negative number.'''
    for n in numbers:
        if n < 0return n
    return 0
 
sum = 0
for numbers in a_list_of_numbers:
    sum += find_first_negative(numbers)

改寫後,兩個 if 都不見了,主邏輯很清楚地表現出「找出各 list 第一個負數並加總」。

同樣的,程式愈複雜時,這些寫法省下的思考時間愈可觀。

明確的指明 else 的處理方式

這和前面提的東西有一點相衝突,視情況而定。在任何有 if、elif 的情況,即使 else 的情況不需做任何處理,仍要明確的寫出 else 並加上註解。如下所示:

1
2
3
4
5
6
if some_condition:
    ...
elif another_condition:
    ....
else:  # Do nothing.
    pass

這個作法的目的是,讓其他讀這份程式的人,明白原作者沒有漏考慮 else 的情況,不處理是符合預期的作法。函式愈長時,這樣寫的好處愈明顯。別小看這個小動作,程式碼一多,回頭讀程式碼時,這點小動作可以省下不少分心的機會。

結語

上面提的例子背後的目的都一樣,就是避免讀程式碼的人分心在分支裡,而能專注在主邏輯上。類似的例子還有「使用 iterator 少用 for + index」,平時留意一些小細節,不但能愈寫愈快 (省去煩心細節的時間),也能降低維護成本,讓其他人易於理解。舉手之勞做環保,大家一起來維護程式碼的品質吧!

2010年2月10日 星期三

觀察實驗資料和畫圖表的小技巧

之前在《減少操作實驗浪費的時間》提到一些做實驗的小技巧,方便自動化重覆實驗、觀察結果和產生圖表。最近實驗做得比以前更多,意外發覺更好用的方式 。有需求就會進步,這是工程師的宿命啊!

主要的改變有三點:

  • 用 Python 寫程式執行實驗。Python 功能比 shell script 強上不少,也方便維護和增加功能。「Can I use Python as a bash replacement?」提到這樣做的好處和簡單的實施要點。什麼?你看了上篇文章發現我好像用 Ruby 較多?這...總之我跳槽了。男子漢做事,有時是不需要理由的!
  • 改用 SQLite 當資料庫。使用 local database 至少有兩個好處:每次實驗都重建一個新資料庫,方便保留不同實驗結果;少掉煩人的設定,可以更彈性地使用。實驗後可以用 sqlite3 看內容,或另寫程式下 SQL 彙整結果。若有架 Wiki,直接將結果輸出成 Wiki code 也不壞,容易加上顏色、粗體和表格。
  • matplotlib 畫圖。matplotlib 是一套 Python 繪圖函式庫,能畫長方圖、折線圖、直方圖等多種類型圖片。更重要的是,它畫得還挺不賴的 (見官網範例)!既可以在 matplotlib 內建的應用程式裡看結果,也能存成 PNG 檔。依我個人過去用 gnuplot 的經驗,matplotlib 好用許多。不過這可能取決於使用者有多熟悉 Python。若你也喜歡用 Python,matplotlib 無疑是最好的選擇。「Tools I use: matplotlib」提出相似的見解,有興趣的人可以參考看看。

不論是 SQLite 還是 matplotlib 都有嚇死人詳細的文件,配合範例程式學起來挺輕鬆的。目前唯一的不便之處是,當實驗數據更多時,有時只想看部份東西,再一步步看不同層面。若一口氣把全部資料畫成圖表,不容易閱讀。有空時想來研究看看 Django (殺雞用牛刀嗎?),看能不能寫個簡單網頁,做為互動呈現數據的介面。反正資料存在 SQLite 裡,容易用各種方式存取資料。

2010年2月5日 星期五

學 Python 的入門書

常看到有人問這問題,想說來寫篇心得,省得重覆回一樣的問題。

我在學 Python 前已學過別的程式語言,所以想找針對已學過程式的人的書。一開始翻許多人推薦的《Learning Python》,發現它是針對沒學過程式的人,有太多篇幅在介紹基本觀念 (如 if 是什麼 ),翻沒幾頁就沒耐性看下去。而《Programming Python》又太厚了,於是找別本書。

後來 Scott 推薦我看《Python Essential Reference》 (目前出到第四版,包含 Python 2.6 的新功能),我一看就驚為天人,「if、elif、else」只有一頁!內容包含完整語法,以及特殊情況 ( 若 if 之後想放空的 statement ,要用 pass )。沒錯,這就是我要的書,去蕪存菁地讓讀者立即掌握 Python 的語法。附帶一提,Scott 明明已學會了,竟然還買了一本用來傳教,真是面惡心善的好學長。

看完這本後,有時查些 Python 觀念或函式名稱,發現常連到《Dive into Python》,才發覺這本也滿有名的,而且還有完整免費的網頁版。於是又找時間看了《Dive into Python 3》,順便了解 Python 3 有啥有趣的東西。發覺這本書超合我胃口,直接用完整的例子說明語法,在學到語法的同時,也明白怎麼實際使用這些語法。有些書就像辭典一般,提了很正式的語法,讀起來很累,讀完卻不知能怎麼用它們。《Dive into Python 3》不但用生動的實例避免這個問題,並進一步深入淺出地介紹運作細節。而且還提到如重構、單元測試等寫軟體的重要觀念,又有詳細的 Python 2 和 3 的比較。若只對 Python 2 有興趣也沒關係,大部份語法仍適用於 Python 2.6 (2.6 是承接 Python 2 和 3 的橋樑)。

總結來說,若你沒學過程式語言,我不知道能推薦什麼書藉。或許可以參考《Python Programming: An Introduction to Computer Science》,這是 MIT 6.00: Introduction to Computer Science and Programming 的參考書,該課程用 Python 教沒寫過程式的學生學會電腦科學家的思考方式。若有學過程式語言,《Python Essential Reference》或《Dive into Python 3》都值得一看。除了學會 Python 之外,兩者提供不同的額外內容,都看也不會浪費時間。

延伸閱讀

學會 Python 基本語法後,可以先看這幾篇了解如何寫出更道地的 Python。道地的 Python 程式不但易讀、易維護,通常也表示更有效率:

再來,可以到《Python Essential Reference》作者 David Beazley 的網站挖寶,像是:

另外若對 design patterns 有興趣,可以看 Google 員工 Joe Gregorio 寫的 (The Lack of) Design Patterns in Python

《Python Cookbook》也有不少經典寫法,不過該書有點舊了 (到 Python 2.4),有些方法已用不到,看的時候要挑一下。

原以為延伸閱讀應該沒多少東西,沒想到我拉拉雜雜也看了不少文件,整理到後來就累了。不知何時 Python 才會像 Java 有本《Effective Python》,一本搞定大部份的注意事項和經典寫法。

備註:設定開發環境

詳見《學習和開發 Python 的好幫手》

2010年1月5日 星期二

Python 加速的方法

這一年來用 Python 開發滿爽的,用久後發覺 Python 真的是不夠快啊。雖說這就是 trade-off,拿 CPU 執行時間換工程師開發的時間,但關鍵時刻跑得不夠快,還是得投入時間研究如何加速。程式開發就是一連串的 trade-off,人生也是如此啊!…………………還是少嘴炮,切入正題吧。

用 Profiling 測全體執行時間

去除演算法、資料結構的問題外 (像是 O(N log(N)) vs. O(N^2) 的方法 ),千萬別自己亂猜效能瓶頸就一頭熱地改程式。隨便想想都能舉出一堆自以為聰明的反例。我印象最深的經驗是,有一次我在寫資料結構作業答案,題目是實作 hash table 來統計文章裡單字出現的次數,身為課程助教,程式當然不能跑得太慢啊。所以調了一下效能,結果發覺改了一些演算法 (如 hash table 的 「chain」用 binary tree 而不用 linked-list ),卻沒快多少 (變慢是常有的事),一用 profiler 量,卻發現 1/3 的時間花在把一行文字切成單字上。我原本用 Java 的 split 一次切完回傳一個 List,改用 StringTokenizer 後就省下整體 1/3 的時間。

Python 有內建 profiling 的工具,叫做 profile,可以透過命令列直接下參數使用:

1
python -m cProfile -s time MODULE.py ARGUMENTS

這樣會執行 MODULE 再將測量結果輸出到螢幕,時間依單一函式執行時間來排序。引用官網的例子,cProfile 的輸出格式如下:

1
2
3
4
5
6
7
8
     2706 function calls (2004 primitive calls) in 4.504 CPU seconds
 
Ordered by: standard name
 
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    2    0.006    0.003    0.953    0.477 pobject.py:75(save_objects)
 43/3    0.533    0.012    0.749    0.250 pobject.py:99(evaluate)
 ...

前面的 tottime 是該函式自己執行的時間;後面的 cumtime 則是該函式整體 (包含它呼叫其它函式) 的執行時間。

附帶一提,以前試 Java profiling 工具時,執行速度會慢個數倍,但用 Python 的 cProfile 卻只變成一倍多慢而已,也可能我那時用的 Java 工具太糟,或參數沒設好吧。

用 timeit 來量單一 expression 的速度

timeit 也是內建模組,也可以用命令列執行:

1
python -m timeit EXPRESSION

下面是執行的例子:

1
2
3
4
5
$ python -m  timeit 'range(10)'
1000000 loops, best of 3: 0.3 usec per loop
 
$ python -m  timeit 'xrange(10)'
10000000 loops, best of 3: 0.166 usec per loop

我比較常寫程式來測 timeit,透過命令列執行 expression 相當受限,還是自己寫段程式包成函式後,再用 timeit 執行較方便。附上一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python
# Count numbers in a list.
# Demonstrate built-in functions' efficiency.
 
import timeit
import random
 
# prepare test data
numbers = range(2)
a = []
for n in numbers:
   a += [n] * 10000
random.shuffle(a)
 
def test():
   c = {}
   for x in a:
       c[x] = c.get(x, 0) + 1
 
def test2():
   c = {}
   for n in numbers:
       c[n] = 0
   for x in a:
       c[x] += 1
 
def test3():
   c = {}
   for n in numbers:
       c[n] = a.count(n)
 
 
if __name__=='__main__':
   t = timeit.Timer("test()", "from __main__ import test")
   print 'test', t.timeit(number=1000)
 
   t = timeit.Timer("test2()", "from __main__ import test2")
   print 'test2', t.timeit(number=1000)
 
   t = timeit.Timer("test3()", "from __main__ import test3")
   print 'test3', t.timeit(number=1000)

有興趣的人可以猜猜看那個方法最快 [1]。

找出瓶頸後加速的方法

用 profiling 找出瓶頸後,大概有幾種作法:

  • multiprocessing 平行化:這年頭 CPU 不用錢,隨便買都是 multi-core。若程式可以單純平行化,這個作法比用 C/C++ 方便一些,至少還是在寫 python code。但若程式稍微複雜,寫起來會很辛苦,且容易因 fork 或 lock 的 overhead 而沒快多少。
  • 將 CPU bound 的地方透過 ctypes 改寫成 C/C++:雖說 ctypes 很好用,不過寫 C/C++ 得注意的事比寫 Python 多太多了,所以我滿少這麼幹的。
  • 從 Python interpreter 下手,改用較快的寫法:這個作法需要一些 Python 運作的知識,才知道怎麼改會比較快。比方若不需用到整個 list 可以改用 generator 減少產生 list 的時間、使用 member field 存取資料以減少 function call、改用 built-in 函式組出要做的事 (如上面附的例子)。這時 timeit 就很好用了,可以先測好關鍵部份,減少重跑整個 profiling 的時間。

不勞而獲的加速方法

我只有偶而注意相關消息,還沒試過,只知道各家方法有些不適用的問題,這裡記些關鍵字。

Psyco 是老字號工具,大家都說讚,對數值運算特別有效,但它只支援 32-bit CPU,作者目前在開發 PyPy。另外 Google 開發的 unladen-swallow 也令人期待。也許在不久的將來,兩邊都會有足以實用的成果吧。

延伸閱讀

Victor 告知,Python 官方 Wiki 有幾篇超棒的文件,相當值得一看:

備註

  1. 這是我執行出來的數據:
    1
    2
    3
    
    test 5.41639614105
    test2 2.70715403557
    test3 0.619280815125

    即使走訪 list 兩次,方法三還是比前兩個方法 (只走訪一次) 快上不少,這是因為方法三用 C 實作的程式走訪迴圈,而前兩個方法用到 Python 的迴圈。

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年7月22日 星期三

學程式語言的樂趣

國三時為了做北市數學科展,實習老師教我使用 Microsoft Word,這是我首次體會到電腦在遊戲之外的樂趣。為了學會怎麼灌 Windows,高一時參加了社團,結果社團學長 Scott 說:「灌 Windows 太簡單了,你來學 C 吧。不過我們這裡只有 Red Hat Linux,就邊學 C 邊學 Linux 吧。」傻傻的我,以為 Linux 裡只有 Vi,就四處翻 Linux 入門書,看完各本書裡 Vi 部份,還寫了篇 Vi 的教學。日漸熟悉 Vi、bash 指令後,覺得玩系統、寫程式真有趣。於是上課無聊就偷看電腦書,下課耍自閉繼續看書,中午放學都跑去社團混,就這樣渡過我的高一。不知不覺,我打下日後學習能力最重要的基礎。

高一一時想不開,同時學 C 和 Perl,用 Perl 寫東西頗有趣的,特別是費盡心思把程式擠到一行作完一件煩人的小工作時,有說不出來的成就感。可是我基礎太弱,無法明白兩者更深層的差異,只能當作兩個不同工具看。兩者同時學的下場,學一陣子仍不熟兩邊的語法。於是我先專心學 C,之後 Perl 也忘光了。那段日子最大的收獲是,不要一次同時學兩個語言。另外,多虧 Scott 心理不正常,用 K&R 的《C 程式語言》當入門書,我直接學會標準語法,和 C 的典型寫法。這個習慣一直伴隨我到現在,每學一個新語言時,我傾向直接找能教我典型寫法的書,省去先花時間學一堆語法,再去蕪存菁地寫出典型風格。

寫了三年的 C,大一接觸到 Java 後,覺得 OOP 很有趣,這麼簡單又優雅地,讓我能同時使用兩個 Stack!雖然一開始寫著滿滿的 C-like Java,程式裡都是 static method,看了些 OOP 的書,開始轉型寫著自以為是 OOP 的怪東西。時至今日,我仍然不明白要怎麼寫出真正的 OOP。至少我發現自己寫得不是 OOP,和大學時相比,算是進步不少。

有些課要求用 C++,我就會用 Java 學到的 OOP 觀念和 C 的經驗來寫。每次要寫 C++ 就重翻一下 C++ 快速入門的書,寫完後又忘光一切。大三做專題時,我和朋友們打算參加一個國外的比賽,由於主要贊助商是 Microsoft,我們決定用 .NET Framework 開發。就這樣,我有個好機會寫 C#。亂翻一些 ASP.NET 的書把殼刻一刻後,我找了《C# Essentials》來看。這本書如其名,薄又都是重點,很快地明白 C# 的特色,讓我覺得 C# 挺漂亮的。不過寫完專題後沒繼續寫 C#,現在什麼也不記得了。而且,當時我的底不夠深,仍無法感受到各語言的特色。

除 C++、C# 兩個插曲外,大學時主要用 Java 和 PHP。用途很簡單,接網站案子用 PHP,其它用 Java。我練習 PHP 的方式是不斷改寫自己的網站,還有接些小案子,可是我覺得 PHP 實在不怎麼美,沒動力學好它,能用就好。

大學中途有試著寫了一點 Python、Scheme,可惜無法持續。一直到碩一一時興起,一方面是厭煩了用 PHP 寫重覆的東西,另一方面想試看看當紅的 Ruby on Rails,就邊查邊寫完成一個小案子。寫得過程裡,覺得 Ruby 很奇妙,於是在寒假花了一週讀完《Programming Ruby》第一部份,這才正式開始我學的第三個語言。

自那之後,我只有寫 Ruby,Java、PHP和其它語言碰也沒碰,這樣直到碩士畢業。沒錯,我的碩士論文也是用 Ruby 寫的,這才深深體會到 Ruby 有多慢有多肥,幸好實驗室的機器很壯,不然我得改用 Java 完成碩論。這段期間,把一個論文裡用到的程式零零總總地加起來,大概有兩千多行,也驚覺 Ruby 幫我省了不少寫程式的時間,相信用 Java 的話,我無法在不到一個月的時間裡寫完自己的演算法、數個比較對象的演算法、前置處理、評估分數等程式。不過卻辛苦交接的學妹,這樣瘋狂用一個不熟語言趕出來的程式碼,原以為不會繼續用的說。

用 Ruby 讓我有了第二次衝擊,就像當年從 C 到 Java 發現可以同時有兩個 Stack 那般驚奇,沒想到 iterator、code block 這麼好用,腦裡想什麼,手就可以直接敲出來,不用寫一寫還要把游標移來移去。而且,Ruby 可以寫出很短又好讀的程式,我從來沒想過程式可以寫到這麼精簡,而寫到這麼精簡後,又更容易理解。

碩士畢業去一家公司實習時,該公司不用 Ruby,我藉機來試看看 Python。於是,對照先前一年多寫 Ruby 的經驗,我明白很多事。有時選擇某個語言,沒有什麼明確的理性依據,單純是個人偏好。高中時我一直想知道 Python 和 Perl 的差異,到底學那個比較划算,如今又多了一個 Ruby 列入抗爭。現在來看,即使我可以找到三者在語言設計、執行速度、社群、函式庫、上手度等各方面的比較,到頭來,選擇何者反而像是信仰。

附帶一提,若要推廣一個程式語言,良好的互動直譯器、快速隨手查的文件、精簡易讀的入門書,三者缺一不可。 Ruby 大概就互動直譯器弱了一些,而 Python 三者都完美無缺,非常容易上手。至於 Java,即使到現在,我仍不知道要推薦別人看什麼書入門。最後附上我對各語言入門方式的看法,入門書書單是針對已學過一種語言的人:

語言 互動式直譯器 隨手查的文件 入門書
C man page 《C 程式語言》
Ruby 設定後的 irb ri 《Programming Ruby》
Python ipython ipython 《Python Essential Reference》
Java CHM格式的 Java doc

( 待續 )

2009年5月1日 星期五

Python 排序的技巧

用 Python 排序時相較於 C、Java 多了效率議題。由於 Python 內建函數 (built-in function) 是用 C 實作的,排序時盡可能用內建函數效率會快上不少,詳細的數據比較可以參見 Python Cookbook,這篇做個簡單的整理。

背景知識

本篇需要 list comprehensionmap 的基本觀念,若不清楚的話可以先了解它們再往下看。

排序 list 和 dict

先來看程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
d = { 'a': 2, 'b': 1 }
# return keys ordered by key
print sorted(d)                                      # ['a', 'b']
# return values ordered by key
print map(d.get, sorted(d))                          # [2, 1]
# return keys ordered by value
print sorted(d, key=d.get)                           # ['b', 'a']
# return values ordered by value
print sorted(d.values())                             # [1, 2]
# return (key, value) pairs order by key
print sorted(d.items())                              # [('a', 2), ('b', 1)]
# return (key, value) pairs order by value
print sorted(d.items(), key=operator.itemgetter(1))  # [('b', 1), ('a', 2)]

上面的程式列出所有排序 dict ( 在一些語言裡稱為 hash table) 的方法,這裡用到幾個技巧,分別說明一下。

return values ordered by key:Python 的 method call 其實和 function call 差不多,obj.func(x) 相當於傳回 func(obj, x),所以 d.get 一樣可以傳進 map 裡,結果是會取回 d 的 value。關於 function 和 method 的說明,可以參考《理解 python 的 method 和 function 兼谈 descriptor 》

return keys ordered by value:同上的技巧,注意參數 key 有做最佳化處理,使得 d 的每個元素只會呼叫一次 d.get ( 而不是比較次數 O(N*logN) 次 ),不用擔心取 key 的函式會被重覆執行。

return (key, value) pairs order by value:這部份的 code 和排序 list 一樣,關鍵在於用了 operator.itemgetter,它的文件說明摘錄如下:

Return a callable object that fetches the given item(s) from its operand.
After, f=itemgetter(2), the call f(r) returns r[2].

以程式碼來表示的話,類似於這麼寫:

1
2
def itemgetter(n):
    return lambda r: r[n]

只要資料轉成 list of lists 的形式,就可以用 operator.itemgetter 處理。轉成 list of lists 的時間和資料數量成正比,相對於套入用 python 寫的 comparator 來說,快上不少。module operator 裡有許多好用的函式,在套用 map、max、sort 等內建函式時,可以配合使用,一來不用自己重寫小函式,二來內建函式用 C 實作,速度快上許多。

Decorate-Sort-Undecorate (DSU)

若要比較的方式比較複雜,像是希望有 stable sort,或是比較的 key 不只一個,可以套用 DSU 的解法。這裡以 stable sort 為例 (程式引用自 Python Cookbook):

1
2
3
4
def stable_sorted_copy(alist, _indices=xrange(sys.maxint)):
    decorated = zip(alist, _indices)
    decorated.sort()
    return [ item for item, index in decorated ]

DSU 的關鍵在於先把每筆資料轉成 [key1, key2, ..., original item] 的形式,如此一來內建的 sort 就會依 key1, key2, … 的順序排序,排好後再取回 original item。同樣的,排序 object 依欄位值或 method 傳回值,都可以套用 DSU。而 stable sort 的情況比較不同,是將資料轉成 [original item, original position ],排序後再取回 original item。

附帶一提,xrange 是 generator,使用 generator 好處可以參見 PEP 289: Generator Expressions

參考資料

備註

發表這篇後,才發現去年的今天我在寫 Ruby。物換星移,忽然有淡淡的懷舊感傷。

2009年4月25日 星期六

寫 Blog 好幫手 TD-Post!

為了方便寫 Blog,寫了個小程式讀 wiki code 產生 WordPress 吃的格式,順便藉機練習 TDD,沒想到寫了十小時才完成。剛好最近在看虎x龍的動畫,就將它命名為TD-Post (Tiger x Dragon Post) 吧。

緣起

換過多家 WordPress 用的編輯器後,我還是找不到滿意的工具。為縮短寫 Blog 的時間,我決定自己做一個。

寫 Blog 最惱人的事有幾點:

  1. 得輸入麻煩的 HTML,特別是要匹配結束標籤特別麻煩。而且 HTML code 不易閱讀,改文章時很不方便。
  2. 加超鏈結很麻煩,常用的幾個超鏈結,得重附貼多次,像 TDD 我就重貼了許多次。
  3. 無法用自己慣用的編輯器。若能用 Vim 寫 Blog,速度必能大增啊!

功能簡介

對我來說,寫 wiki code 相當方便,所以輸入格式決定用 wiki code。但 wiki 格式有許多種,我比較常用的有 PmWikiDokuWikiTWiki,其中以 PmWiki 語法最簡單,但原始碼沒後兩者好讀。最後聽從 York 的建議,採用 Tracwiki 格式。選它的主要原因除好讀好寫外,我特別喜歡它 Preformatted Text 的格式,很適合用來貼程式碼。

接著,針對第二點問題,加上自動補超鏈結的功能,只要寫過一次超鏈結,TD-Post 就會自動記下來,自動補上對應的位置。像在這段裡,因為我在第一段已寫過 TD-Post 的位置, TD-Post 的超鏈結都會自動補上。初步估計,以後寫一篇 Blog 至少可以省十分鐘,所以差不多用個六十次...這幾天的辛勞就.就回本啦。

程式下載

本程式採 BSD License ( 簡單說就是隨便使用 ),歡迎大家玩玩。雖然沒什麼註解,但我照 TDD 流程寫的,測試碼應該很完整。這裡是一些相關資訊:

  • 下載:http://code.google.com/p/td-post/downloads/list
  • 程式語言:Python
  • 程式碼總行數:682 行
  • 測式碼:346 行 ( 主要是準備測試的例子 )
  • 非測試碼:336 行
  • 實作時間:原本預估兩小時完成,卻花了約十小時。大概是一小時查語法、一到兩小時寫測試。其中花最多的時間在解決 list 和 performatted text 的衝突,這部份從開始實作到結束花了三小時才解決。

其它

有原始碼有真相,最後附上本文的原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
為了方便寫 Blog,寫了個小程式讀 wiki code 產生 [http://wordpress.org/ WordPress] 吃的格式,順便藉機練習 [http://en.wikipedia.org/wiki/Test-driven_development TDD],沒想到寫了十小時才完成。剛好最近在看[http://zh.wikipedia.org/wiki/虎與龍_(小說) 虎x龍]的動畫,就將它命名為[http://code.google.com/p/td-post/ TD-Post] (Tiger x Dragon Post) 吧。
 
==== 緣起 ====
換過多家 [WordPress] 用的編輯器後,我還是找不到滿意的工具。為縮短寫 Blog 的時間,我決定自己做一個。
 
寫 Blog 最惱人的事有幾點:
 1. 得輸入麻煩的 HTML,特別是要匹配結束標籤特別麻煩。而且 HTML code 不易閱讀,改文章時很不方便。
 1. 加超鏈結很麻煩,常用的幾個超鏈結,得重附貼多次,像 [TDD] 我就重貼了許多次。
 1. 無法用自己慣用的編輯器。若能用 [http://www.vim.org/ Vim] 寫 Blog,速度必能大增啊!
 
==== 功能簡介 ====
對我來說,寫 wiki code 相當方便,所以輸入格式決定用 wiki code。但 wiki 格式有許多種,我比較常用的有 [http://www.pmwiki.org/ PmWiki]、[http://www.dokuwiki.org/ DokuWiki]、[http://twiki.org/ TWiki],其中以 [PmWiki] 語法最簡單,但原始碼沒後兩者好讀。最後聽從 York 的建議,採用 [http://trac.edgewall.org/ Trac] 的 [http://trac.edgewall.org/wiki/WikiFormatting wiki 格式]。選它的主要原因除好讀好寫外,我特別喜歡它 Preformatted Text 的格式,很適合用來貼程式碼。
 
接著,針對第二點問題,加上自動補超鏈結的功能,只要寫過一次超鏈結,[TD-Post] 就會自動記下來,自動補上對應的位置。像在這段裡,因為我在第一段已寫過 [TD-Post] 的位置, [TD-Post] 的超鏈結都會自動補上。初步估計,以後寫一篇 Blog 至少可以省十分鐘,所以差不多用個六十次...這幾天的辛勞就.就回本啦。
 
==== 程式下載 ====
本程式採 [http://zh.wikipedia.org/w/index.php?title=BSD许可证&variant=zh-tw BSD License] ( 簡單說就是隨便使用 ),歡迎大家玩玩。雖然沒什麼註解,但我照 [TDD] 流程寫的,測試碼應該很完整。這裡是一些相關資訊:
 * 下載:http://code.google.com/p/td-post/downloads/list
 * 程式語言:[http://www.python.org/ Python]
 * 程式碼總行數:682 行
 * 測式碼:346 行 ( 主要是準備測試的例子 )
 * 非測試碼:336 行
 * 實作時間:原本預估兩小時完成,卻花了約十小時。大概是一小時查語法、一到兩小時寫測試。其中花最多的時間在解決 list 和 performatted text 的衝突,這部份從開始實作到結束花了三小時才解決。
 
==== 其它 ====
 
有原始碼有真相,最後附上本文的原始碼:{{{
#!html
_QUINE_
}}}
 
身為資工人,這段原始碼當然也是自動貼上的啦,不過我沒用像 [http://en.wikipedia.org/wiki/Quine_(computing) Quine] 那樣的技巧,只是單純地取代關鍵字。

身為資工人,這段原始碼當然也是自動貼上的啦,不過我沒用像 Quine 那樣的技巧,只是單純地取代關鍵字。

2009年4月22日 星期三

強化 Python 在 Vim 裡的顏色

我習慣用 putty 連 Unix server 開 screen,再用 vim 寫 Python。這篇記錄如何改善 Python 的顏色。

啟動 256 色 terminal

首先將可用的色彩數增加為 256 色,先確定 putty 為新版 ( 舊版 putty 沒支援 256 色)。接著參照《讓 Vim、Screen 支援 256 色》將 Screen 和 Vim 設好,記得先用文末提到的 Colortest 測試是否有符合 256 色,成功的話,數字 0 ~ 15 為原本的 16 色,16 ~ 231 為新的顏色 (6 x 6 x 6),232 ~ 255 為新的灰階色。可配合 Colortest 看到數字值和顏色。

這裡備忘該篇提到的步驟:

  • 在 ~/.vimrc 加入:
    1
    
    set t_Co=256
  • 在 ~/.screenrc 加入:
    1
    
    termcapinfo xterm 'Co#256:AB=\E[48;5;%dm:AF=\E[38;5;%dm'

更新 Python script 偵測的格式

更新 syntax/python.vim,讓 Vim 偵測出更多種格式,像是行末多的空白,或是空白行有含空格都會被偵測出來。做法如下:

  1. 下載最新的 python.vim 並放到 ~/.vim/syntax/ 下。
  2. 編輯 ~/.vimrc,加入
    1
    
    let python_highlight_all = 1

    以顯示所有 python.vim 有偵測的格式。參照 python.vim 開頭的註解,了解細部選項。

替換顯示的顏色

  1. 下載 Vim color scheme: Wombat256.vim,並放到 ~/.vim/colors/ 下。
  2. 在 ~/.vimrc 裡加入
    1
    
    colorscheme wombat256

如此一來就有漂亮的顏色啦,wombat256 的 screenshot 見這裡

備註

這個網站有提供許多不同的 color scheme screenshot,可惜沒附 Python 的 screenshot。

2009年4月19日 星期日

最近用 Python + TDD 心得(與 Java 做對照)

最近又用 Python 寫了些程式,剛好和之前用 Java 進行 TDD 做個對照。

若不知道 TDD 的人,可以先參考這篇,TDD 的概念是依以下三個步驟寫程式 ( 順序相當重要 ):

  1. 先寫簡單的單元測試,並執行它。
  2. 用最簡單的方法實作需要的功能,讓程式能通過測試。
  3. 重構程式,並確保重構後的程式仍能通過測試。

實作部份不需多談,這裡先分別對步驟一「寫測試碼」和步驟三「重構」討論,再分享一般性的一點心得。

寫測試碼

Python 寫起測試碼比 Java 簡單許多,可以輕易地抽換所有既有物件,如 module、function、class、method。下面是一個簡單的抽換 method 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass(object):
    def hello(self, msg):
        print msg
 
def new_hello(self, msg):
    print msg + " (by replaced method)"
 
obj = MyClass()
obj.hello('Testing rocks!')  # Testing rocks!
mystub = stub.Stub()
mystub.replace(MyClass, 'hello', new_hello)
obj.hello('Testing rocks!')  # Testing rocks! (by replaced method)
mystub.restore(MyClass, 'hello')
obj.hello('Testing rocks!')  # Testing rocks!

註解表示該行輸出結果,其中 Stub 是我寫得一個簡單 class,只有十行多,可以替換和還原物件。可以從 MyClass.hello() 的輸出看出,stub.replace() 後 MyClass.hello() 被換為 new_hello(),stub.restore() 後則換了回來。完整的程式碼請看這裡

方便抽換物件有什麼好處呢?舉例來說,若 class A 有兩個 method B 和 C,其中 B 用到 C ( B 會視 C 傳回結果改變程式流程 )。為了方便測試 B 的行為,得先控制 C 的傳回結果。若是 Python 的話,照上面的例子透過 Stub ( setattr() ) 替換 A.C 即可。但在 Java 的情況,除非將 C 當作參數傳給 B,讓測試程式有機會傳「假的 C 」給 B,不然難以控制 B 內部行為。寫新程式時還有機會改設計,對於舊的程式,修改程式是個災難。用到別人的函式庫或沒程式碼就無解了。不論 Java 有何解法 ( 如採用 injection 的方式 ),解法愈麻煩,表示大眾愈不願意做,因此降低測試碼的品質,連帶影響 TDD 效果。

Mock library 部份,Java 有 EasyMock,Python 有 Mox,兩者用法差不多。後者是前者改來的,學一套兩邊都可以用。

重構

由於 Java 的特性 ( static type ),重構工具相當成熟,我慣用 Eclipse 的重構功能,離開它就不太想寫 Java。其中我最常用的功能是改名稱,包含 variable、method 、 class、 package 等。好的名稱是好程式碼的必要條件之一,好的名稱可以省去冗長的註解、縮短理解程式的時間,同時也有助於作者釐清邏輯,減少犯錯的機會。若發現名稱很難取,也許表示該 object(method / class / package)功能沒規範好,之後容易遇到問題。寫程式難免會犯錯,寫一段時間後才發現命名不夠精確,得回頭修改。若有好的重構工具,改起來快又沒風險,可以提高重構的意願。

反觀 Python 因為 dynamic type,程式碼本身提供的訊息不足(得執行後才清楚全貌),難以透過工具重構。我找了一下相關工具後沒看到滿意的。Eclipse 的 PyDev plugin 有提供重構,但試用後結果是錯的,改 module 名稱時沒動到檔名,執行後才會發現程式碼爛了。另外試了 Rope ,可以正確改名,可是速度有點慢,操作相當不直覺 ( 之後再來試看看 ropevim )。為了長遠發展考量,或許可以試看看自己弄個簡單版的改名工具或改 Rope,工具的功能可以少,但要快、容易操作且結果正確。

結語

只有在三個步驟都確實做到時,TDD 才能發揮應有的威力。依我目前一點點的實作心得,Python 容易寫測試碼、卻不方便重構;而 Java 正好相反。現在我大多有照 TDD 的流程寫程式,即使寫個一小時的小程式,也會用 TDD 。有時寫寫覺得卡卡的,才發現忘了先寫測試碼。

另外,測試碼的範圍抓得準 ( 別測太細,也別懶得測核心) ,效果才會好。我初用 TDD + Python 時就矯枉過正,寫太多測試變成 「over-testing」,省了 over-design 的負擔,卻多花時間寫不必要的測試碼、又增加日後維護的成本 。

養成 TDD 習慣的關鍵,在於寫測試碼的功力,像是如何準備 fixture ,如何改善函式介面以利測試。為了寫測試而改變函式介面並不是本末倒置,通常這會降低函式之間的關聯性,將功能明確切開,讓每個函式的輸入輸出都很乾淨(有簡單的輸入,才方便準備 fixture)。而乾淨的輸入輸出意味著函式更容易被組合使用。

2009年3月8日 星期日

讓 Python 的 unittestgui 可以載入 test suite

由於 pyUnit 附的 unittestgui 怎麼試都沒成功,我改用這個 unittestgui.py:GUI test runner using wxPython。相關說明請見之前的文章。美中不足的是,它不方便載入 test suite,所以我修改了一下這份程式。

程式下載和用法

點此下載修改後的程式碼。使用的範例程式碼如下:

1
2
3
4
5
6
7
8
9
# all_test_suite.py
import unittest
import catsinbag.card_factory_test
import catsinbag.cash_test
 
def suite():
    suites = []
    suites.append(unittest.TestLoader().loadTestsFromTestCase(some_module.SomeTestCase))
    return unittest.TestSuite(suites)

執行 unittestgui.py 後,按 Open 選擇 all_test_suite.py 即可載入。

目前這個工具仍有些問題,像是原始碼改過後,得重開檔案載入後才會生效,所以無法一邊改程式碼一邊按 Run Test。還有錯誤訊息顯示得不夠清楚,而且還是顯示在 console 上,不是顯示在 GUI 上。

修改過程心得

參照 unittest官方文件,最方便載入多個 test cases 和 test suites 的方法,是採用下面這段 code:

1
2
3
suite1 = unittest.TestLoader().loadTestsFromTestCase(SomeTestCase1)
suite2 = unittest.TestLoader().loadTestsFromTestCase(SomeTestCase2)
alltests = unittest.TestSuite([suite1, suite2])

若自己用 console 執行 unittest,只要把 alltests 交給 TextTextRunner 即可,例如:

1
unittest.TextTestRunner(verbosity=2).run(alltests)

但是,一個熱愛寫 testing 程式的程式設計師,除了想要有更高品質的程式碼外,內心更渴望看到 Green Bar啊!所以非得用 GUI 版的 unittest 不可。而 unittestgui.py 可以載入 test case、test suite,卻沒辦法載入 module 內產生的 test suite。所以只好直接改 unittestgui.py 的程式碼。關鍵在於 OnMenuOpen() 如何取得 test suite。原本的程式碼如下:

1
2
3
4
5
6
7
modname, modtype = os.path.splitext(os.path.basename(self.filename))
if modtype.lower() == '.py':
    moduleToTest = imp.load_source(modname, self.filename, file(self.filename))
elif modtype.lower() in {'.pyc':0, '.pyo':0}:
    moduleToTest = imp.load_compiled(modname, self.filename, file(self.filename, 'rb'))
#print moduleToTest, dir(moduleToTest)
self.suite = unittest.defaultTestLoader.loadTestsFromModule(moduleToTest)

修改後的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
modname, modtype = os.path.splitext(os.path.basename(self.filename))
if modtype.lower() == '.py':
    moduleToTest = imp.load_source(modname, self.filename, file(self.filename))
elif modtype.lower() in {'.pyc':0, '.pyo':0}:
    moduleToTest = imp.load_compiled(modname, self.filename, file(self.filename, 'rb'))
#print moduleToTest, dir(moduleToTest)
if hasattr(moduleToTest, 'suite'):  # use the prepared suite if it exists.
    if callable(moduleToTest.suite):
        self.suite = moduleToTest.suite()
    else:
        self.suite = moduleToTest.suite
else:
    self.suite = unittest.defaultTestLoader.loadTestsFromModule(moduleToTest)

利用已讀到的 module: moduleToTest,增加判斷它是否有提供 function suite() 或是 attribute suite,若有的話,直接使用準備好的 test suite,而非從 TestCase 或 TestSuite 中產生。

最後附上執行後的精美圖片,辛苦了這麼久,就是為了看到這 Green Bar 啊!

unittestgui_with_suite

2009年2月18日 星期三

Windows 下初寫 Python 心得

今天下午用了一下 Python 處理簡單的文字檔,結果激起我對 Python 沉封已久的渴望,晚上看個半集動畫後,就試著在 Windows 下寫看看 Python。

除 Java 外,多年來我都習慣在 Unix 下寫程式,用 Windows 時也是透過 putty 連入 Unix,開 screen + vim split 來寫。這次轉念一想,乾脆來試看看在 Windows 下開發的情況,順便增長見聞。

保險起見,我選擇用 Python 2.6 而不是最近剛出來的 Python 3000。安裝時會要求選個路徑,我先開個 D:Python,之後打算把東西都丟在這下面,所以就裝在 D:PythonPython26下。裝好後的第一印象是,Windows 版有精美的 chm 檔真好,索引文件超方便!

再來是設定 VIM:

  • 下載 python.vim到 C:Documents and SettingsUSERvimfilesindent ( 需先自行新增目錄 vimfiles 和 vimfilesindent ),並在 C:Documents and SettingsUSER_vimrc 裡加上:
    1
    
    autocmd BufRead,BufNewFile *.py set sw=4 tabstop=4 smarttab smartindent expandtab

    如此一來才能方便地縮排和有漂亮的色彩。

  • 為了方便執行,在 _vimrc 裡加上:
    1
    
    autocmd BufRead,BufNewFile *.py map <F10>% w !python<CR>

    這樣就能按 F10 執行目前編輯的 script。

編輯環境弄好後開始試 Test Driven Development ,來看 pyUnit 怎麼用。pyUnit 有含在預設的 modules 裡,抄幾段範例碼就能用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-------------------------------------------------
# Press F10 to execute the test case directly in VIM
#-------------------------------------------------
 
import unittest
 
class MainTestCase(unittest.TestCase):
    def setUp(self):
        pass
 
    def tearDown(self):
        pass
 
    def testAMethod(self):
        self.assertTrue(True)
        self.assertEqual(2, 1 + 1)
 
 
if __name__ == "__main__":
    unittest.main()

結果沒找到 GUI 版的 pyUnit,少了精美的綠色進度表就不是 xUnit 了啊!

接下來演變為長期抗戰,裝好的 Python26 裡沒有 pyUnit 上提到的 unittestgui.py,而官網上的載點也失效了,好在可以從 code search site 找到: unittestgui.py。可是怎麼試都是載入失敗。最後在 pyUnit 官網找到另一個方案:GUI test runner using wxPython

這份 unittestgui.py 用到 wxPython,所以得下載對應到 Win32 Python 2.6 的 wxPython,執行安裝檔後會自動找到 Python 2.6 的位置,而將預設路徑設為 Python26Libsite-packages,真是方便啊。原本還在擔心 Windows 下 Python module 要怎麼管理,結果很簡單。果然無知會造成恐懼。裝好 wxPython 後,再改一下 wxPython 版的 unittestgui.py (加上 import sys),就可以用漂亮的 GUI 版 Unit test 工具啦!而且是選檔案直接載入 test case,不用辛苦地填入 package/module path。

unittestgui_wxPython

附帶一提,site-package 的設計簡單易懂,了解後以後要手動裝 module 也不成問題啦!

最後提一下 IPython 的相關訊息。IPython 比原本 python 內建的 interpreter 強太多了,是寫 python 的必備工具之一。IPython 首頁有列出下載的頁面,點 xxx.exe 下載執行,自然會裝到 site-packages 裡。官網首頁下方有指出 Windows 下要另外裝 PyReadline 才能正確執行 IPython,不然會沒顏色,且按 Tab 補字時會丟出 exception。IPython 的執行方式有幾種,這裡列出其中兩種:

  • console mode:python Python26Scriptsipython
  • GUI mode:python Python26Libsite-packagesIPythonfrontendwxipythonx.py

雖然是一樣的東西,總覺得 Unix 下用起來比較方便。

2009-02-23 更新

Scott Tsai 提醒,修正文中和 vim 相關的說明。沒想到才剛在 Plurk 提到「以後提到 Scott Tsai 可以加超連結了」,馬上就用到了。

2009-03-08 更新

修正 vim indent 的目錄路徑,並補上 IPython 的相關訊息。

2008年8月31日 星期日

近兩個月使用 Python 心得 + 從 Ruby 對照的心得

最近兩個月都在寫 Python,反而 Ruby 快忘光了...

由於先前有 Ruby 的底子,轉學 Python 的進入門檻低了許多,一年前我和 Scott 聊過,我提些 Ruby 功能,Scott 就提 Python 要怎麼寫;或是反過來進行。當時就覺得這兩個語言的功能很像。經過兩個月的練習,現在我反而比較熟 Python,真是諷刺啊。

個人覺得 Python 和 Ruby 的主要不同是哲學層次的,Ruby 講求人性,給予使用者 (programmer) 極大的彈性;Python 則是有個明確的規則在,講求一致性。「Form is liberating」,我並不討厭 Python 的做法。舉例來說,Ruby 會提供許多別名方法 (alias method),讓使用者可以憑直覺用對功能,比方加一個元素到陣列裡,在 Ruby 裡有以下的寫法:

a = [] a.push 3 a.push(3) a << 3

但在 Python 裡,就只有 a.append(3) 一種。並且,Ruby 讓使用者可以在任何時刻改變任意物件的內容。所以 Ruby 的使用者不用擔心拿不到 third party library 的 source code,反正有啥難處理的地方,在 runtime 直接修改即可。方便歸方便,在大型專案裡意味著寫完函式庫後,作者無法保證使用該函式庫的人會讓函式庫元件照規矩運作,這使得程式碼協同運作充滿危機。

相較於 Ruby 強調直覺帶來的便利,Python 則是制定好最佳規則,照規則辦事。以 exception 來說,Python 遵從有意外就用 exception 的規則,除以零是 exception,dictionary (即 hash table)取用不存在的 key 也是 exception,取用不存在的變數也是 exception。反觀 Ruby,以人性的角度出發,這三者狀況的反應分別為:exception、nil (Ruby 的 null)、exception。從這可看出 Ruby 對 hash table 取用不存在的 key 的行為不一致,但這麼做確實比較方便,省去先查 key 是否存在再取值的動作。另外以查 method 為例,Ruby 的查法很直接(直覺):

[].methods.grep /push/

[].methods 就會顯示 [] 所有方法; methods.grep 就會對 methods 做字串比對。反觀 Python 則是透過 __dict__ 的機制達成:

[s for s in dir([]) if "p" in s] # dir() 會傳回 [].__dict__ 和 [] 父類別的 __dict__

Python 另對 __dict__ 有許多有趣的設計,像是 hasattr(),還有讓 __dict__ 替 function 帶來許多好處 (如 cache,可做到類似 C 裡 function 的 static variable 的效果),這部份太細了,不在此談論。

另一方面,兩者都強調 Everything is object,但對 object 的定義不同,Python 對 object 的定義可參見:《Python Types and Objects》:The Object Within。Ruby 的 OO 很強,Python 沒有 private/protected/package-private/public 這類流行的 OO scope 字眼,反而用變數命名前加底線,暗示使用者有底線開頭的物件是 private/protected,但使用者仍能直接存取。對我來說,我認為 private/protected/package-private/public 實在太難設了,若非經驗豐富的程式設計師,很難訂對,所以不訂也還過得去,省下煩惱 scope 的時間。

除 Unicode 支援良好外,Python 另一令我激賞的特性是 self-document。Python 規定 module、class、function 的文件必須寫在 module 的開頭、class 的下一行、function 的下一行。造成的結果是,許多工具如 ipython 能輕易地將程式碼內的文件顯示出來。只有有些寫程式和文件經驗的人就能明白,將文件和程式分開寫是多麻煩的事,通常下場是文件漏寫,或是文件過期。所以文件和程式愈能合為一體,愈有助於程式碼品質 (code quality)。

其它如 thread 的差異,和 C/C++ 結合的容易度,我還沒碰到無法比較,到是 interpreter 部份值得一提。個人認為好的入門書、好的文件和 interpreter 可大幅減低入門門檻,是否有好書取決於作者功力,這部份兩邊都有不錯的書;文件部份如前所述,Python 的設計相當棒,;而 interpreter 部份,ipython 實在太強了,像 Scott 是直接在 ipython 上寫,再存成 script 檔。經過這兩個月的洗禮,我決定先投向 Python 陣營,只好忍痛割捨 Ruby 寫碼的爽快感。

2008年7月15日 星期二

Python class mthod

Python 原本沒有 class method,只有 instance method,這在一些情況下不方便,比方 factory method。後來 Python 的改版做了修正,詳細的說明見 static methods,以及後來的改良語法 decorators,兩篇文章的說明淺顯易懂,我就不在這轉述了。感謝 Aethanyc 介紹。

decorator 不只可改善 classmethod 語法,還可以協助當 function wrapper,用來做 log、檢查參數。官網的例子很讚,值得一看,這裡摘錄一段 singleton 的做法:

def singleton(cls): instances = {} def getinstance(): if cls not in instances: instances[cls] = cls() return instances[cls] return getinstance @singleton class MyClass: ...

有興趣的話可以對照 Ruby 版的 cache,Python 應該也能寫出類似的東西。我很喜歡這種由外包裝的寫法,寫起來很乾淨,完全不會影響到原 function 以及 call function 的 code。

2008年7月13日 星期日

查 Python 函式的方法

學程式語言的首要問題就是如何查函式,個人認為有良好的查函式介面,才容易推廣程式語言。

像 Java 有 JavaDoc,可從 Sun 官網查詢(個人喜歡用別人包好的 Windows Help 檔);PHP 官網也有便利的查詢介面;C 在 Unix 上可用 man page(硬漢工程師才用 C,啃點 man page 不是問題!); 個人覺得Ruby的官網文件不怎麼好查,不過用 Ruby 直接查到是滿快的。比方我記得有個 p 開頭的函式可以消掉陣列最後一個元素,開個 irb(interactive interpreter)這麼打即可:

[].methods.grep /p/

查到正確名稱後,再用 ri 查詳細用法,相當順手。

Python 在這方面弱了一些,這裡整理一下我目前的做法(從 Scott 那學來的)。首先,一定要裝 ipython(功能強大的 interactive interpreter),之後就可以利用 code completion 來查名稱,例如:

n [2]: a = “”

In [3]: a.sp
a.split a.splitlines

In [3]: a.split?
Type: builtin_function_or_method
Base Class:
String Form:
Namespace: Interactive
Docstring:
S.split([sep [,maxsplit]]) -> list of strings

Return a list of the words in the string S, using sep as the
delimiter string. If maxsplit is given, at most maxsplit
splits are done. If sep is not specified or is None, any
whitespace string is a separator.

注意,直接打 “”.sp 再按 TAB 是沒用的,因為 ipython 要按下 ENTER 後才能猜測先前輸入過的變數型別為何。名稱後加「?」可以叫出 __doc__ 內的說明。

若想像 Ruby 那樣搜尋特定函式名稱,得透過 __dict__ 或直接用 built-in function dir(),__dict__ 位存有 class 內的 methods 和 fields,下面的效果和 Ruby 相同:

print [s for s in dir([]) if "p" in s]