翻譯:《實用的Python程式設計》03_03_Error_checking

codists發表於2021-03-02

目錄 | 上一節 (3.2 深入函式) | 下一節 (3.4 模組)

3.3 錯誤檢查

雖然前面已經介紹了異常,但本節補充一些有關錯誤檢查和異常處理的其它細節。

程式是如何執行失敗的

Python 不對函式引數型別或值進行檢查或者校驗。函式可以處理與函式內部語句相容的任何資料。

def add(x, y):
    return x + y

add(3, 4)               # 7
add('Hello', 'World')   # 'HelloWorld'
add('3', '4')           # '34'

如果函式中有錯誤,它們將(作為異常)在執行時出現。

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>

為了驗證程式碼,強烈建議進行測試(稍後介紹)。

異常

異常用於發出錯誤訊號。

要自己觸發異常,請使用 raise 語句:

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

要捕獲異常,請使用 try-except 語句:

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

異常處理

異常傳遞到第一個匹配的 except

def grok():
    ...
    raise RuntimeError('Whoa!')   # Exception raised here

def spam():
    grok()                        # Call that will raise exception

def bar():
    try:
       spam()
    except RuntimeError as e:     # Exception caught here
        ...

def foo():
    try:
         bar()
    except RuntimeError as e:     # Exception does NOT arrive here
        ...

foo()

要處理異常,請將語句放到 except 塊裡面。 except 塊裡面可以新增要處理該錯誤的任何語句。

def grok(): ...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   # Exception caught here
        statements              # Use this statements
        statements
        ...

bar()

異常處理之後,從 try-except 之後的第一個語句繼續執行。

def grok(): ...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   # Exception caught here
        statements
        statements
        ...
    statements                  # Resumes execution here
    statements                  # And continues here
    ...

bar()

內建異常

有非常多的內建異常。通常,異常名稱表明出了什麼問題(例如,因為提供錯誤的值而觸發 ValueError)。下述列表不是一份詳盡的清單,請訪問 文件 以獲取更多資訊。

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

異常值

異常具有一個關聯值。它包含有關錯誤的更明確的資訊。

raise RuntimeError('Invalid user name')

這個值是異常例項的一部分,它被放置在提供給 except 的變數中。

try:
    ...
except RuntimeError as e:   # `e` holds the exception raised
    ...

e 是異常型別的一個例項。但是,當列印的時候,它通常看起來像一個字串。

except RuntimeError as e:
    print('Failed : Reason', e)

捕獲多個異常

可以使用多個 except 塊捕獲不同型別的異常:

try:
  ...
except LookupError as e:
  ...
except RuntimeError as e:
  ...
except IOError as e:
  ...
except KeyboardInterrupt as e:
  ...

或者,如果處理不同異常的語句是相同的,則可以對它們進行分組:

try:
  ...
except (IOError,LookupError,RuntimeError) as e:
  ...

捕獲所有的異常

要捕獲所有的異常,請使用 Exception 。如下所示:

try:
    ...
except Exception:       # DANGER. See below
    print('An error occurred')

通常,像這樣編寫程式碼是個壞主意,因為這說明不知道程式為什麼會失敗。

捕獲異常的錯誤方式

這裡是一個使用異常的錯誤方式。

try:
    go_do_something()
except Exception:
    print('Computer says no')

這將捕獲所有可能的錯誤,並且,當程式碼因為某些根本沒想到的原因(如解除安裝 Python 模組等)執行失敗時,可能無法進行除錯。

更好的方式

如果想要捕獲所有的錯誤,這有一個更明智的方法。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

它報告了失敗的明確原因。當編寫捕獲所有可能異常的程式碼時,擁有檢視/報告錯誤的機制幾乎總是一個好主意。

不過,通常來說,最好在合理的範圍內儘量窄地捕獲異常。僅捕獲能處理的異常。讓其它錯誤通過——也許其它程式碼可以處理。

重新觸發異常

使用 raise 傳遞捕獲的錯誤。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

這允許你採取措施(例如:記錄日誌)並將錯誤傳遞給呼叫者。

異常的最佳實踐

不要捕獲異常,而是失敗發生時“停止執行,發出預警”(Fail fast and loud)。如果重要的話,別人會處理的。只有你是那個人的時候才捕獲異常。即,只捕獲可以恢復並正常執行的錯誤。

finally 語句

finally 語句指定無論是否發生異常都必須執行的程式碼。

lock = Lock()
...
lock.acquire()
try:
    ...
finally:
    lock.release()  # this will ALWAYS be executed. With and without exception.

通常使用 finally 語句安全地管理資源(尤其是鎖,檔案等)。

with 語句

在現代程式碼中,try-finally 語句通常被 with 語句取代。

lock = Lock()
with lock:
    # lock acquired
    ...
# lock released

一個更熟悉的例子:

with open(filename) as f:
    # Use the file
    ...
# File closed

with 語句定義資源的使用上下文。當執行離開上下文時,資源被釋放。with 語句僅適用於經過專門程式設計以支援它的某些物件。

練習

練習 3.8:觸發異常

在上一節中編寫的 parse_csv() 函式允許選擇使用者指定的列,但是隻有輸入資料檔案具有列標題時才會生效。

請修改程式碼,以便在同時傳遞 selecthas_headers=False 引數時觸發異常。例如:

>>> parse_csv('Data/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

新增此檢查後,你可能會問是否應該在函式中執行其它型別的完整性檢查。例如,檢查檔名是字串,列表還是其它型別?

一般來說,最好是跳過此類測試,輸入錯誤的時候讓程式執行失敗。回溯資訊會指出問題的根源,並且幫助除錯。

新增上述檢查的主要原因是為了避免在無意義的模式下執行程式碼(例如,使用要求列標題的特性,但是同時指定沒有標題)。

這表明呼叫程式碼部分出現一個程式設計錯誤。檢查“不應發生”的情況通常是個好主意。

練習 3.9:捕獲異常

你編寫的 parse_csv() 函式用於處理檔案的全部內容。但是,在現實世界中,輸入檔案可能包含損壞的資料,丟失的資料或者髒資料。嘗試下面這個實驗:

>>> portfolio = parse_csv('Data/missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

請修改 parse_csv() 函式以便捕獲所有在記錄建立期間生成的 ValueError 異常,併為無法轉換的行列印警告訊息。

錯誤訊息應該包括行號以及有關失敗原因的資訊。要測試函式,嘗試讀取上面的 Data/missing.csv 檔案,例如:

>>> portfolio = parse_csv('Data/missing.csv', types=[str, int, float])
Row 4: Couldn't convert ['MSFT', '', '51.23']
Row 4: Reason invalid literal for int() with base 10: ''
Row 7: Couldn't convert ['IBM', '', '70.44']
Row 7: Reason invalid literal for int() with base 10: ''
>>>
>>> portfolio
[{'price': 32.2, 'name': 'AA', 'shares': 100}, {'price': 91.1, 'name': 'IBM', 'shares': 50}, {'price': 83.44, 'name': 'CAT', 'shares': 150}, {'price': 40.37, 'name': 'GE', 'shares': 95}, {'price': 65.1, 'name': 'MSFT', 'shares': 50}]
>>>

練習 3.10:隱藏錯誤

請修改 parse_csv()函式,以便使用者明確需要時可以隱藏解析的錯誤訊息,例如:

>>> portfolio = parse_csv('Data/missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'price': 32.2, 'name': 'AA', 'shares': 100}, {'price': 91.1, 'name': 'IBM', 'shares': 50}, {'price': 83.44, 'name': 'CAT', 'shares': 150}, {'price': 40.37, 'name': 'GE', 'shares': 95}, {'price': 65.1, 'name': 'MSFT', 'shares': 50}]
>>>

在大部分的程式中,錯誤處理是最難做好的事情之一。一般來說,不應該默默地忽略錯誤。相反,最好是報告問題,並且讓使用者選擇是否隱藏錯誤資訊(如果它們選擇這樣做)。

目錄 | 上一節 (3.2 深入函式) | 下一節 (3.4 模組)

注:完整翻譯見 https://github.com/codists/practical-python-zh

相關文章