翻譯自StackOverflow中一個關於Python異常處理的問答。
問題:為什麼“except:pass”是一個不好的程式設計習慣?
我時常在StackOverflow上看到有人評論關於except: pass的使用,他們都提到這是一個不好的Python程式設計習慣,應該避免。可我想知道為什麼?有時候我並不在意出現的錯誤,而是隻想讓我的程式繼續進行下去。就像這樣:
1 2 3 4 |
try: something except: pass |
為什麼這麼使用except:pass不好?這背後的原因是什麼,是不是因為這樣我會放掉一些本該被處理的錯誤?還是這樣我會捕獲到所有型別的錯誤?
最佳回答:
正如你所猜測的那樣,這麼做的確有兩個不好的地方。首先,因為沒有指定任何異常型別,所以會捕獲到任何型別的錯誤。其次,捕獲到錯誤之後只會簡單地讓它通過而不是採取必要的處理措施。
我接下來的解釋或許會有點長,所以將重點總結如下:
1. 不要將任意型別的錯誤作為捕獲物件。必須明確你想要捕獲的錯誤型別,並且寫明只捕獲它們。
2. 不要試圖簡單地敷衍錯誤處理動作。除非這麼做是有目的的,但這通常都不太好。
那麼接下來讓我們更深入一些:
不要將任意異常作為捕獲目標
當在程式碼中的某個地方使用異常捕獲語句塊時,你通常知道這個地方可能會丟擲異常,並且你也知道這個地方可能會發生什麼樣的問題進而丟擲何種異常,一旦異常被丟擲,你將捕獲到這個異常並使程式回到正軌上來。這就意味著你一定對這種異常有所準備,並能夠在它發生的時候及時採取措施進行處理。
舉個例子,你需要使用者輸入一個數字,並且使用int()函式將使用者輸入的字串轉換為整數型別,這時候你一定會想到如果輸入的字串並不是數字,那麼就會發生值錯誤(ValueError)。如果真的發生了錯誤,那麼你可以通過簡單的讓使用者重新輸入來讓程式回到正軌,所以捕獲值錯誤以及促使使用者重新輸入就是一個比較合理的處理策略。再舉一個例子,如果你想從一個檔案中讀取配置資訊,但正巧這個檔案不存在。那麼因為這是一個配置檔案,如果它不存在你會返回一些預設的配置選項,所以這個檔案就不是這麼必要了。在這個例子中,捕獲檔案未找到錯誤(FileNotFoundError)以及返回預設配置項則是一個比較合適的處理策略。通過以上兩個例子可以看到,我們都是在等待捕獲特定的錯誤,並且針對每種錯誤都有特定的處理策略。
然而,如果我們在這裡捕獲所有的異常,那麼為特定異常準備的那些處理策略就會因為遇到非正常型別的異常而失效,這將會使得正常的程式流程中斷並且無法恢復。
讓我們還是舉配置檔案的那個例子。正常的處理策略是如果發現檔案並不存在,我們將使用預設的配置項,並可能在稍後決定是否將當前的配置項自動儲存為配置檔案(這樣的話下一次檔案就肯定存在了)。現在讓我們假定我們捕獲到了一個IsADirectoryError或是PermissionError錯誤,在這種情況下,我們可能不想讓程式繼續執行,我們仍然能夠使用預設的配置引數,但是隨後我們就不能儲存檔案了。也有可能使用者希望使用自定義的配置項,所以這樣的話就不能使用預設配置項了。所以我們這時候可能需要立即告知使用者並停止當前程式。也有可能我們並不想在這麼一小塊程式碼中做這麼多的事情,而是讓應用層面的部分去關心它,所以我們也可能讓這個錯誤浮到頂層,讓頂層的業務邏輯去處理。
在Python 2 idioms document文件中也提到了一個簡單的例子:如果在我們的程式碼中出現了一個簡單的拼寫錯誤而導致程式錯誤。在這種情況下因為我們捕獲所有的異常,所以我們將會捕獲到名稱錯誤(NameErrors)以及語法錯誤(SyntaxErrors)。兩者都是常見的錯誤,並且兩者都是不希望出現在我們最終程式碼中的。但是因為我們什麼異常都逮,當異常發生時我們將無法區分具體的錯誤型別並且無法進行除錯。
但是也存在這樣一些並未預先準備的危險異常,諸如系統錯誤(SystemError)就很少發生以至於我們根本沒有準備;這些異常通常需要更復雜的處理操作,這些操作通常可能會要求我們停止當前的工作。
在任何情況下,通過區域性程式碼實現對所有異常的處理基本上都是不可能的,所以你應該有針對性的去處理那些經過準備的特定異常。有些人曾建議至少應該明確指明基本異常(Exception)這樣的不包含諸如系統退出(SystemExit)和鍵盤中斷(KeyboardInterrupt)這樣設計用來終止應用程式的異常。但是我想說這樣還是不夠明確,並且我個人認為只有在一個地方才能僅僅只捕獲Exception或是任何型別的異常,那就是一個單獨的,應用程式層面的異常捕獲器,並且這個捕獲器唯一的任務就是去捕獲任何可能出現的未經準備的漏網異常。這樣的話我們仍然能夠保留意外發生異常的相關資訊作為進一步的程式碼擴充套件的依據(讓然如果我們能讓程式恢復的話),這樣下一次我們就能夠把這個異常在合適的地方顯式地指定出來或是指導我們撰寫測試用例以保證錯誤不再發生(當然了,這一切還是要當我們對特定異常有所準備時,沒有準備的異常還是會溜掉)。
在異常處理的邏輯中,不要什麼都不做
當顯式地捕獲到有限的幾個異常之後,很多時候我們的確不需要做什麼特別的處理。這種情況下,except SomeSpecificException: pass這麼做是可以的。但是大多數情況下,我們還是需要一些與錯誤恢復相關的程式碼,例如重複嘗試的動作以及設定預設值。
同時也考慮到其它情況,例如如果我們的程式碼結構已經確定了必須不斷嘗試直到成功才能繼續,那麼什麼也不做就已經夠了。具體來說,我們需要使用者輸入一個數字,因為我們知道使用者可能不會按照我們設計的那樣做,所以我們會將這個部分放入一個迴圈,比如像這樣:
1 2 3 4 5 6 |
def askForNumber (): while True: try: return int(input('Please enter a number: ')) except ValueError: pass |
因為我們會不斷讓使用者輸入直到沒有異常丟擲為止,這種情況下我們就不需要在except塊中做其他任何特別的操作,這樣就夠了。當然了,有人會說你至少應該讓使用者得到一些錯誤資訊以明確他們為什麼在此被反覆地要求輸入。
在其他一些情況下,except塊中的passing語句顯示了我們並沒有真正的對異常做好準備。除非是一些簡單的異常(諸如值錯誤ValueError或型別錯誤TypeError)我們都應該做一些操作,原因也很明顯,避免簡單的passing。如果真沒什麼可做的(如果你真的確定),那麼考慮加一些解釋性的註釋在此;否則,請擴充套件except塊新增一些恢復性的程式碼。
except: pass
最不能容忍的就是兩者的結合了。這意味著我們自願捕獲任何異常(包括那些我們沒有準備的)並且對它們視而不見。你應該至少在日誌中記錄一下這個錯誤並且向上提出來終止當前程式(我就不信出現MemoryError你的程式依然能正常執行)。放過這些異常將會使程式在錯誤的軌道上繼續執行下去並且丟掉了關鍵的錯誤資訊從而使得錯誤不易被發現,特別是當不是你親自遇到它的時候。
所以,底線是去捕獲那些經過準備的特定異常;其他發生的異常要麼是等著你去修復的錯誤,要麼是你無法處理的。當真的沒有什麼可做的時候放過某些特定異常是可以的,其他情況如果這麼做就只能被認為是怠工或偷懶了。你的確應該去處理這些異常的。