最近在維護程式時對於 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 == 1: return 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 < 0: break if n < 0: sum += 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 < 0: return 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」,平時留意一些小細節,不但能愈寫愈快 (省去煩心細節的時間),也能降低維護成本,讓其他人易於理解。舉手之勞做環保,大家一起來維護程式碼的品質吧!
GOOD
回覆刪除