Python 3 Tutorial 第五堂(1)Shit happens!


程式難免會發生錯誤,在先前的幾堂課中進行的練習雖然簡單,然而你一定也遇上了一些錯誤,你會怎麼處理這些錯誤呢?提供更清楚的錯誤訊息?事先檢查運行條件以避免錯誤?或者是在錯誤發生後,趕緊收拾善後,避免浪費系統資源?

「怎麼處理錯誤」是很值得也必須思考的課題,如果你從未在這方面認真過,建議日後若有時間,可以參考一下以下幾篇文章:

tryexcept

來個實際的例子吧!在〈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..exceptexcept 可以指定多個物件,也可以有多個 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

在語法方面若想進一步瞭解,可以參考:

實際上,語法是語法,更重要的是思考如何處理例外,這篇文件中僅僅提供了其中一個思考過程,如果你想認真地面對錯誤,別忘了看看這篇文件一開頭提到的那幾篇文章。