程式難免會發生錯誤,在先前的幾堂課中進行的練習雖然簡單,然而你一定也遇上了一些錯誤,你會怎麼處理這些錯誤呢?提供更清楚的錯誤訊息?事先檢查運行條件以避免錯誤?或者是在錯誤發生後,趕緊收拾善後,避免浪費系統資源?
「怎麼處理錯誤」是很值得也必須思考的課題,如果你從未在這方面認真過,建議日後若有時間,可以參考一下以下幾篇文章:
try
、except
來個實際的例子吧!在〈Python 3 Tutorial 第二堂(1)Unicode 支援、基本 I/O〉中,我們看過讀取檔案的一個範例:
import sys
for line in open(sys.argv[1], 'r'):
print(line, end = '')
這個範例可以使用命令列引數,指定要讀取的檔案名稱,如果沒有提供命令列引數的話,就會由於 argv[1]
沒有元素,而發生存取超出索引範圍的錯誤:
Traceback (most recent call last):
File "test.py", line 2, in <module>
for line in open(sys.argv[1], 'r'):
IndexError: list index out of range
在 Python 中程式若發生錯誤會引發例外,以上例而言就是引發 IndexError
,如果程式沒有處理例外而丟出至執行環境,則會顯示例外追蹤(Traceback)並中斷程式。如果想要處理例外,則可以使用 try...except
語句。
對於沒有指定檔案,接下來的讀取檔案流程就無法進行,因此對這個例外最好的處理方式,就是指示使用者應當提供檔案名稱,例如:
import sys
try:
for line in open(sys.argv[1], 'r'):
print(line, end = '')
except IndexError:
print('請提供檔案名稱')
print('範例:')
print(' python3.5 read.py your_file')
如果使用者輸入錯誤,引發的 IndexError
物件會被 except
比對型態是否相同,如果相同則執行對應的區塊。以上例而言,如果使用者輸入錯誤,就會顯示較友善的提示訊息(而不是丟個使用者看不懂的追蹤訊息)。
try..except
的 except
可以指定多個物件,也可以有多個 except
,如果沒有指定 except
後的物件型態,則表示捕捉所有引發的物件。
舉例來說,底下的範例中,若使用者於(Linux 中)輸入時輸入 Ctrl + D,會引發 EOFError
,若輸 入 Ctrl + C,則會引發KeyboardInterrupt
:
import traceback
try:
input = int(input('輸入整數:'))
print('{0} 為 {1}'.format(input, '奇數' if input % 2 else '偶數'))
except ValueError:
print('請輸入阿拉伯數字')
except (EOFError, KeyboardInterrupt):
print('使用者中斷程式')
except:
print('不明的程式中斷')
traceback.print_exc()
由於 except
會捕捉所有引發的物件,必須置於最後。traceback.print_exc()
則可以顯示例外的追蹤訊息。
finally
對於先前讀取檔案的範例,如果指定的檔案名稱不存在的話,open
函式會引發 FileNotFoundError
,對於這個錯誤,顯示給使用者較清楚的錯誤訊息,應該會是比較好的處理:
import sys
try:
file = open(sys.argv[1], 'r')
for line in file:
print(line, end = '')
file.close()
except IndexError:
print('請提供檔案名稱')
print('範例:')
print(' python3.5 read.py your_file')
except FileNotFoundError:
print('找不到檔案 {0}'.format(sys.argv[1]))
在〈Python 3 Tutorial 第二堂(1)Unicode 支援、基本 I/O〉中曾經說過,在 open
傳回的實例被回收後,檔案就會關閉,不過,你沒辦法預期回收的時機,因此,檔案關閉的時機也就無法預期,如果你想在讀檔後,馬上關閉檔案以節省資源,可以如上頭,明確地呼叫 file.close()
。
然而,這個範例有點問題,如果讀取檔案的過程中發生了錯誤,那麼就不會執行到 file.close()
,若你想確保檔案一定會被關閉,可以定義在 finally
之中。例如:
import sys
try:
file = open(sys.argv[1], 'r')
try:
for line in file:
print(line, end = '')
finally:
if file:
file.close()
except IndexError:
print('請提供檔案名稱')
print('範例:')
print(' python3.5 read.py your_file')
except FileNotFoundError:
print('找不到檔案 {0}'.format(sys.argv[1]))
如上頭看到的,try...except...finally
可以形成巢狀結構,讀檔是在順利開檔之後要進行的流程,因此,使用了另一層 try...finally
來進行,如何針對不同層次的流程,處理不同層次發生的錯誤,這需要你依實際需求來思考。
raise
當然,在 try...except...finally
形成巢狀時,可讀性並不好,這時一個可行的處理方式是,將內層的流程封裝為一個函式,例如:
import sys, logging
def for_each_line(file, do_action):
try:
for line in file:
do_action(line)
except:
logger = logging.getLogger(__name__)
logger.exception('未處理的例外')
finally:
file.close()
try:
file = open(sys.argv[1], 'r')
for_each_line(file, lambda line: print(int(line) + 10, end = ''))
except IndexError:
print('請提供檔案名稱')
print('範例:')
print(' python3.5 read.py your_file')
except FileNotFoundError:
print('找不到檔案 {0}'.format(sys.argv[1]))
在這個例子中,for_each_line
使用 except
捕捉所有的例外,然後使用 Python 的 logging
API,以logger.exception()
記錄了例外訊息,不過,由於 except
捕捉例外並進行了處理,這個例外也就沒有再向外傳播了。
在這邊日誌處理,僅僅只是為了留下記錄,便於日後除錯,你也許希望例外能向外傳播,例如,你也許是在 GUI 程式中呼叫了 for_each_line
,若它發生未處理的例外,想要能捕捉例外並以視窗顯示錯誤訊息,你可以使用 rasie
來引發例外:
import sys, logging
def for_each_line(file, do_action):
try:
for line in file:
do_action(line)
except:
logger = logging.getLogger(__name__)
logger.exception('未處理的例外')
raise
finally:
file.close()
try:
file = open(sys.argv[1], 'r')
for_each_line(file, lambda line: print(int(line) + 10, end = ''))
except IndexError:
print('請提供檔案名稱')
print('範例:')
print(' python3.5 read.py your_file')
except FileNotFoundError:
print('找不到檔案 {0}'.format(sys.argv[1]))
except ValueError:
print('檔案中每一行,必須是代表整數的字串')
raise
之後若沒有接上任何指定的例外,就是將 except
捕捉到的例外重新引發,你可以使用 raise
自行引發例外。例如:
try:
raise EOFError
except EOFError:
print('EOFError')
可以在 except
捕捉到例外後,將例外物件指定給變數。例如:
try:
raise IndexError('11')
except IndexError as e:
print(type(e), str(e))
raise e
在語法方面若想進一步瞭解,可以參考:
實際上,語法是語法,更重要的是思考如何處理例外,這篇文件中僅僅提供了其中一個思考過程,如果你想認真地面對錯誤,別忘了看看這篇文件一開頭提到的那幾篇文章。