天天看點

為什麼“except:pass”是一個不好的Python程式設計習慣?

問題: 為什麼“except:pass”是一個不好的程式設計習慣?

我時常在StackOverflow上看到有人評論關于

except: pass

的使用,他們都提到這是一個不好的Python程式設計習慣,應該避免。可我想知道為什麼?有時候我并不在意出現的錯誤,而是隻想讓我的程式繼續進行下去。就像這樣:

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這麼做是可以的。但是大多數情況下,我們還是需要一些與錯誤恢複相關的代碼,例如重複嘗試的動作以及設定預設值。

同時也考慮到其它情況,例如如果我們的代碼結構已經确定了必須不斷嘗試直到成功才能繼續,那麼什麼也不做就已經夠了。具體來說,我們需要使用者輸入一個數字,因為我們知道使用者可能不會按照我們設計的那樣做,是以我們會将這個部分放入一個循環,比如像這樣:

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你的程式依然能正常運作)。放過這些異常将會使程式在錯誤的軌道上繼續運作下去并且丢掉了關鍵的錯誤資訊進而使得錯誤不易被發現,特别是當不是你親自遇到它的時候。

是以,底線是去捕獲那些經過準備的特定異常;其他發生的異常要麼是等着你去修複的錯誤,要麼是你無法處理的。當真的沒有什麼可做的時候放過某些特定異常是可以的,其他情況如果這麼做就隻能被認為是怠工或偷懶了。你的确應該去處理這些異常的。