很多文件或書都說,不要將例外(exception)處理當成是流程的一部份。不過,若察看一下Python標準程式庫的原始碼,會用到try、except的情況,卻有不少是將例外處理當成是流程的一部份。

也許,可以將這些程式碼當成是某個開發者的一時偷懶,然而,進一步做些調查,卻又有著另一番收獲。

把例外當成流程之一?

嚴格來說,不要將例外處理當成是流程的一部份,應該是源自於我對Java例外處理的理解,一直以來都是這麼根深蒂固的觀念,但是,在學習與使用Python的過程中,你會發現例外處理很少被提及,有些書中甚至只用幾頁篇幅就帶過。

這不單只是Python的情況,大凡動態定型語言都會有這樣的傾向,而Java由於本身堅守著Checked例外的特性,開發者不得不被迫面對例外處理。

那麼,到底Python中哪裡會用到try、except處理例外?我索性認真地翻閱Python標準程式庫原始碼,找出有try、except的部份,除了少數真的是在處理錯誤之外,大多數是做為流程的一部份。舉例來說,有段是這樣的:

try:
nbsp; all = object.__all__
except AttributeError:
  all = None

實際上這段程式碼,可以不透過try、except處理,事先透過hasattr函式來檢查__all__屬性是否存在,也可以完成相同的功能:

all = None
if hasattr(object, '__all__'):
 all = object.__all__

類似的情況還有很多,例如,import某個模組時,若模組不存在,會引發ImportError,然而,在Python中經常使用try、except,在發生ImportError時,改import另一個替代模組。

實際上,Python中只要用到for in來迭代,就會利用到例外了,因為一個迭代器若無法再進行下一次迭代,就會引發StopIteration,只不過for in會靜靜地處理掉這個例外,當作什麼事都沒有發生,就像是在except StopIteration之後,直接pass處理。

引發例外是另一種return

在Java Tutorial的〈What Is an Exception?〉中,談到:「例外是個事件,發生於程式執行時期,會中斷程式指令的正常流程。」

然而,將例外看成是事件的概念,卻是在Python這類的動態定型語言中,比較常被看到。StopIteration是個例子,實際上,在Python的例外繼承架構中,直接繼承BaseException的SystemExit、KeyboardInterrupt、GeneratorExit,基本上也不是錯誤,而比較像是事件通知。

當然,錯誤也是例外的一種,因為若發現某個錯誤將使得程式流程無法繼續,代表錯誤的例外就會發生,如果是呼叫程式庫API,那麼例外會由API引發,不過,何妨想一下,當初API開發者為何要引發這個例外呢?

進一步想想,程式庫的錯誤已經這麼多了,然而有時你在設計API時,不也會自己設計一個例外,並在某些時候引發一個例外嗎?是嫌程式中的錯誤還不夠多?還是你想通知呼叫的一方什麼事情?

當自己想主動引發一個例外時,通常是檢查出發現程式在目前狀態下,無法繼續流程,因此必須讓目前的呼叫返回客戶端,並想辦法讓客戶端清楚地接受到這樣的訊息,也就是說,引發例外就類似一個特別的return,只不過客戶端若不處理這樣的return,那麼,例外就會繼續往上一層呼叫者傳播。

《約耳趣談軟體》的作者就曾經在〈Exceptions〉中,談到:「我認為開發者會被C/C++/Java風格語言的例外吸引,只是因為語法上沒辦法簡潔地讓一個函式呼叫能有多個傳回值。」就這點來說,可以有多重傳回值,且經常在發生錯誤時,讓函式傳回值帶有error的Go語言,似乎比較符合Joel的想法了(可參考我先前專欄〈Go的error與panic〉)。

採用LBYL風格?還是EAFP風格?

這麼看來,引發例外就像是另一種有通知功能的return,開發者知道在某個條件下,程式流程會發生中斷,這也許是個錯誤,也可能只是個通知,總是,呼叫者必須針對這樣的中斷通知所有行動,以事件來比擬的話,try、except就像是在註冊事件處理器了。

〈The art of throwing JavaScript errors〉中,出現一個有趣的比擬,在程式碼的特定點規畫出失敗,總比在預測哪裡會出現失敗來得簡單。

這就像是車體框架的設計,會希望撞擊發生時,框架能以一個可預測的方式潰散,如此一來,製造商方才確保乘客的安全性。從這點來看,對軟體健全(Robustness)程度要求高的Java,在Checked例外上的堅持,確實有其道理。

那麼,如果知道了API開發者會引發哪些例外,該採取try、except在例外發生時加以處理(先前程式範例一),還是事先透過檢查來避免例外發生呢(先前程式範例二)?

〈Write Cleaner Python: Use Exceptions〉中,稱後者風格為LBYL(Look Before You Leap),如果沒檢查完全,一切都是我的錯,而前者為EAFP(Easier to Ask for Forgiveness than Permission),出錯了,我再補救以求寬恕。

在Java中,多半會選LBYL,然而,Python的文化中傾於EAFP,〈Write Cleaner Python: Use Exceptions〉中舉了個例子,示範了LBYL的冗長與囉嗦,之後闡述了EAFP的簡潔性,而簡潔正是Python的哲學。文章中另一方面也談到,相較於其他靜態定型語言來說,在Python中,例外處理的成本比較小,這麼做反而比較好。

把例外當成例外

許多情況下,談到例外就是指錯誤,實際上,似乎可以單純地將例外看成是例外,也就是一個讓程式流程無法正常繼續的通知,開發者可以在這樣的通知下做出處理,當然,這並不是鼓勵濫用例外處理,而是指在能表示程式意圖的情況下善加利用。

舉例來說,想事先測試一個模組是否存在,再予以import以避免引發ImportError,其實是可以透過importlib.find_loader來達到,不過,會需要比較多程式碼,整個程式碼語義也不清楚,然而,使用try、except的版本不但簡潔,還有著「試著import」的語義。例如:

try:
  import some_module
except ImportError:
  import other_module

只要語義簡潔且清楚,就目前撰寫程式多半要求「程式碼會說話」的趨勢下,就算是在其他語言中,也可以考量使用,若真的有效能考量,再來評測調整,不一定要堅持「別將例外處理當成是正常流程」,而可以將例外就當成是例外。

作者簡介


Advertisement

更多 iThome相關內容