python學習筆記(六)異常處理

 

目錄

(一)瞭解異常

0)Python異常的類型

1)引發異常

raise語句

assert語句

(二)處理異常

1)try/except

2)try/except...else

3)try-finally 語句

4)用戶自定義異常

5)異常的繼承架構

6)用到with關鍵字的上下文管理器


 

異常,是一種語言特性,專門用於處理程序執行期間的不正常情況。任何程序都可能在運行時遭遇錯誤,異常的最常見用途就是處理程序執行期間出現的錯誤,但也可以有效用於很多其他目的。Python提供的異常已經很全了,用戶還可以根據自己的目的定義新的“異常”。

Python 有兩種錯誤很容易辨認:語法錯誤和異常。

 

(一)瞭解異常

 

爲了說明異常的概念,請考慮字處理程序會有把文件寫入磁盤的情況,但有可能數據尚未寫完就把磁盤空間耗盡了。解決這個問題的方法有很多。

處理磁盤空間問題的最簡單方案,就是假定寫入任何文件都會有充足的磁盤空間,根本不用去操心。遺憾的是,這幾乎之最常用的方案 。對處理少量數據的小型程序而言,一般還說得過去。但對於運行關鍵任務的程序來說,這是完全不能接受的。

可以不做處理;比不做處理高明一步的錯誤處理方案,是意識到錯誤可能會發生,並利用語言提供的標準機制來定義一種檢測和處理錯誤的方案。實現的方式有很多種,典型的方式是讓每個函數或過程都返回狀態值,標識出這次函數或過程調用是否執行成功。執行成功的結果值,可以通過引用類型的參數回傳。

以上這種方式的程序中,絕大部分的錯誤檢查代碼顯然都是重複的。代碼對每次文件寫入錯誤都要進行檢查,並在檢測到錯誤時將錯誤狀態消息回傳給發起調用的過程。磁盤空間錯誤僅在最外層的函數中進行處理。也就是說,大部分錯誤處理代碼只是“管道”(plumb)代碼,用於將引發錯誤的地方和處理錯誤的地方打通。

但是真正要做的事情其實是要去掉管道代碼,把由系統內置的文件寫入函數生成的錯誤直接傳到錯誤處理函數,交由錯誤處理代碼直接處理。因此,異常機制可以讓代碼更加清晰,錯誤處理得更加到位。

總體而言,Python對待錯誤處理的方式與Java等語言的常見方式不同。那些語言往往是在錯誤發生之前就儘可能地檢查出來,因爲在錯誤發生後再來做處理,往往要付出各種高昂地成本。而Python可能更依賴於“異常”,在錯誤發生之後再做處理。雖然這種依賴看起來可能會有風險,但如果“異常”能使用得當,代碼會更加輕巧,可讀性也更好,只有發生錯誤時纔會進行處理。

產生異常的動作被稱爲引發(raise)或拋出(throw)異常。異常可以由其他函數引發,或者由自己的代碼顯式引發。

響應異常的動作被稱爲捕獲(catch)異常,處理異常的代碼則稱爲異常處理代碼或異常處理程序。

0)Python異常的類型

爲了能夠正確反映引發錯誤的實際原因,或者需要報告的異常情況,可以生成各種不同類型的異常對象。Python3.6提供的異常對象有很多類型:

BaseException                                       #所有異常的基類
    SystemExit                                      #解釋器請求退出
    KeyboardInterrupt
    GeneratorExit
    Exception
        StopIteration
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
            LookupError
                IndexError
                KeyError
            MemoryError
            NameError
                UnboundLocalError
            OSError
                BlockingIOError
                ChildProcessError
                ConnectionError
                    BrokenPipeError
                    ConnectionAbortedError
                    ConnectionRefusedError
                    ConnectionResetError
                FileExistsError
                FileNotFoundError
                InterruptedError
                IsADirectoryError
                NotADirectoryError
                PermissionError
                ProcessLookupError
                TimeoutError
            ReferenceError
            RuntimeError
                NotImplementedError
                RecursionError
            SystemError
            TypeError
            ValueError
                UnicodeError
                    UnicodeDecodeError
                    UnicodeEncodeError
                    UnicodeTranslateError
            Warning
                DeprecationWarning
                PendingDeprecationWarning
                RuntimeWarning
                SyntaxWarning
                UserWarning
                FutureWarning
                ImportWarning
                UnicodeWarning
                BytesWarning
                ResourceWarning

Python的異常第項是按層級構建的,上述異常列表中的縮進關係正說明了這一點。每種異常都是一種Python類,繼承自父異常類。

例如,IndexError也是LookupErroor類和Exception(通過繼承),且還是BaseException。

這種層次結構是有意爲之的,大部分異常都繼承自Exception。

1)引發異常

即便 Python 程序的語法是正確的,在運行它的時候,也有可能發生錯誤。運行期檢測到的錯誤被稱爲異常。

大多數的異常都不會被程序處理,都以錯誤信息的形式展現在這裏:

>>>10 * (1/0)             # 0 不能作爲除數,觸發異常
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: division by zero
>>> 4 + spam*3             # spam 未定義,觸發異常
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2               # int 不能與 str 相加,觸發異常
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: Can't convert 'int' object to str implicitly

異常以不同的類型出現,這些類型都作爲信息的一部分打印出來: 例子中的類型有 ZeroDivisionError,NameError 和 TypeError。

錯誤信息的前面部分顯示了異常發生的上下文,並以調用棧的形式顯示具體信息。

Python 使用 raise 語句拋出一個指定的異常,assert語句是raise語句的特殊形式。

raise語句

raise語法格式如下:

raise [Exception [, args [, traceback]]]

以下實例如果 x 大於 5 就觸發異常:

x = 10
if x > 5:
    raise Exception('x 不能大於 5。x 的值爲: {}'.format(x))

執行以上代碼會觸發異常:

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    raise Exception('x 不能大於 5。x 的值爲: {}'.format(x))
Exception: x 不能大於 5。x 的值爲: 10

raise 唯一的一個參數指定了要被拋出的異常。它必須是一個異常的實例或者是異常的類(也就是 Exception 的子類)。

如果你只想知道這是否拋出了一個異常,並不想去處理它,那麼一個簡單的 raise 語句就可以再次把它拋出。

>>>try:
        raise NameError('HiThere')
    except NameError:
        print('An exception flew by!')
        raise
   
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

 

assert語句

Python assert(斷言)用於判斷一個表達式,在表達式條件爲 false 的時候觸發異常。

示例:

def test(ex1, ex2):
    assert (ex1 and ex2), "test false"
    return True

if test(True, False):
    print("is true")
else:
    print("is false")

以上代碼將會有以下輸出:

>>> 
======================= RESTART: D:/python37/assert.py =======================
Traceback (most recent call last):
  File "D:/python37/assert.py", line 5, in <module>
    if test(True, False):
  File "D:/python37/assert.py", line 2, in test
    assert (ex1 and ex2), "test false"
AssertionError: test false

即便 Python 程序的語法是正確的,在運行它的時候,也有可能發生錯誤。運行期檢測到的錯誤被稱爲異常。

 

 

(二)處理異常

 

1)try/except

異常捕捉可以使用 try/except 語句。

 

以下例子中,讓用戶輸入一個合法的整數,但是允許用戶中斷這個程序(使用 Control-C 或者操作系統提供的方法)。用戶中斷的信息會引發一個 KeyboardInterrupt 異常。

while True:
    try:
        x = int(input("請輸入一個數字: "))
        break
    except ValueError:
        print("您輸入的不是數字,請再次嘗試輸入!")

try 語句按照如下方式工作;

  • 首先,執行 try 子句(在關鍵字 try 和關鍵字 except 之間的語句)。

  • 如果沒有異常發生,忽略 except 子句,try 子句執行後結束。

  • 如果在執行 try 子句的過程中發生了異常,那麼 try 子句餘下的部分將被忽略。如果異常的類型和 except 之後的名稱相符,那麼對應的 except 子句將被執行。

  • 如果一個異常沒有與任何的 excep 匹配,那麼這個異常將會傳遞給上層的 try 中。

一個 try 語句可能包含多個except子句,分別來處理不同的特定的異常。最多隻有一個分支會被執行。

處理程序將只針對對應的 try 子句中的異常進行處理,而不是其他的 try 的處理程序中的異常。

一個except子句可以同時處理多個異常,這些異常將被放在一個括號裏成爲一個元組,例如:

except (RuntimeError, TypeError, NameError):
    pass

最後一個except子句可以忽略異常的名稱,它將被當作通配符使用。你可以使用這種方法打印一個錯誤信息,然後再次把異常拋出。

import sys
 
try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

其中except OSError as err:語句中,如果發生了OSError異常,就會創建err變量並且將OSError返回值賦給err變量。

還可以:

def func(num):
    if num < 1:
        raise Exception("Invalid level!", num)


try:
    func(0)
except Exception as err:
    print("No: %s"%(err))

print()
print()
    
def div(dividend, divisor):
    try:
        quotient = dividend/divisor
        return quotient
    except ZeroDivisionError as err:
        print("除數不能爲零啊,哥!",err)

print("div(4,2) => {}" .format(div(4,2)))
print("div(4,0) => {}" .format(div(4,0)))

輸出:

No: ('Invalid level!', 0)


div(4,2) => 2.0
除數不能爲零啊,哥! division by zero
div(4,0) => None

 

2)try/except...else

try/except 語句還有一個可選的 else 子句,如果使用這個子句,那麼必須放在所有的 except 子句之後。

else 子句將在 try 子句沒有發生任何異常的時候執行。

以下實例在 try 語句中判斷文件是否可以打開,如果打開文件時正常的沒有發生異常則執行 else 部分的語句,讀取文件內容:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

使用 else 子句比把所有的語句都放在 try 子句裏面要好,這樣可以避免一些意想不到,而 except 又無法捕獲的異常。

異常處理並不僅僅處理那些直接發生在 try 子句中的異常,而且還能處理子句中調用的函數(甚至間接調用的函數)裏拋出的異常。例如:

>>>def this_fails():
        x = 1/0
   
>>> try:
        this_fails()
    except ZeroDivisionError as err:
        print('Handling run-time error:', err)
   
Handling run-time error: int division or modulo by zero

3)try-finally 語句

try-finally 語句無論是否發生異常都將執行最後的代碼。

以下實例中 finally 語句無論異常是否發生都會執行:

def f1():
    try:
        print(1)
        return 2
    except:
        print(0)
    finally:
        print("這句話,無論異常是否發生都會執行。")
        #return 4

print(f1())

print("-"*24)

def f2():
    try:
        print(1)
        #return 2
    except:
        print(0)
        return(0)
    else:
        print(3)
        return 4
    finally:
        print(5)
        return 6

print(f2())

輸出結果:

1
這句話,無論異常是否發生都會執行。
2
------------------------
1
3
5
6

 

4)用戶自定義異常

你可以通過創建一個新的異常類來擁有自己的異常。異常類繼承自 Exception 類,可以直接繼承,或者間接繼承,例如:

>>>class MyError(Exception):
        def __init__(self, value):
            self.value = value
        def __str__(self):
            return repr(self.value)
   
>>> try:
        raise MyError(2*2)
    except MyError as e:
        print('My exception occurred, value:', e.value)
   
My exception occurred, value: 4
>>> raise MyError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'

在這個例子中,類 Exception 默認的 __init__() 被覆蓋。

當創建一個模塊有可能拋出多種不同的異常時,一種通常的做法是爲這個包建立一個基礎異常類,然後基於這個基礎類爲不同的錯誤情況創建不同的子類:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass
 
class InputError(Error):
    """Exception raised for errors in the input.
 
    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """
 
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message
 
class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.
 
    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """
 
    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

 

 

5)異常的繼承架構

看以下代碼:

try:
    body
except LookupError as err:
    exception code
except IndexError as err:
    exception code

這裏將會捕獲LookupError和IndexError這兩種異常。正巧IndexError是LookupError的子類。如果body拋出IndexError,那麼錯誤會首先被“except LookupError as err:”這行檢測到。因爲IndexError繼承自LookupError,因此第一條except子句會成功執行,第二局except子句永遠不會用到,因爲它的運行條件被第一條except子句包含在內了。

相反,將兩條except子句的順序互換一下,可能就有意義了。這樣第一條子句將處理IndexError之外的LookupError。

 

6)用到with關鍵字的上下文管理器

有些操作場景遵循一種可預期的模式,有始必有終,如文件的讀取操作。讀取文件時,通常只需要打開一次文件,讀取數據後關閉文件。按照異常的編程結構,可以將這種文件訪問代碼編寫如下:

try:
    infile = open(filename)
    data = infile.read()
finally:
    infile.close()

Python3爲這種場景提供了一種更加通用的解決方案,即上下文管理器(context manager)。上下文管理器將代碼包裹起來,對進入(entry)和離開(departure)代碼塊時的操作進行集中管理,用with關鍵字進行標記。文件對象就是一種上下文管理器,可以利用這種能力讀取文件:

with open(filename) as infile:
    data = infile.read()

上下文管理器非常適用於資源加/解鎖、關閉文件、提交數據庫事務之類的操作。更多信息可以查看標準庫中contextlib模塊的文檔,包括如何創建上下文管理器,以及對它的各種操控方式。

 

 

參考:

  1. 菜鳥教程

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章