python基礎之異常處理

Python3 錯誤和異常

做爲Python初學者,在剛學習Python編程時,常常會看到一些報錯信息,在前面咱們沒有說起,這章節咱們會專門介紹。html

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

python標準異常

異常名稱 描述
BaseException 全部異常的基類
SystemExit 解釋器請求退出
KeyboardInterrupt 用戶中斷執行(一般是輸入^C)
Exception 常規錯誤的基類
StopIteration 迭代器沒有更多的值
GeneratorExit 生成器(generator)發生異常來通知退出
StandardError 全部的內建標準異常的基類
ArithmeticError 全部數值計算錯誤的基類
FloatingPointError 浮點計算錯誤
OverflowError 數值運算超出最大限制
ZeroDivisionError 除(或取模)零 (全部數據類型)
AssertionError 斷言語句失敗
AttributeError 對象沒有這個屬性
EOFError 沒有內建輸入,到達EOF 標記
EnvironmentError 操做系統錯誤的基類
IOError 輸入/輸出操做失敗
OSError 操做系統錯誤
WindowsError 系統調用失敗
ImportError 導入模塊/對象失敗
LookupError 無效數據查詢的基類
IndexError 序列中沒有此索引(index)
KeyError 映射中沒有這個鍵
MemoryError 內存溢出錯誤(對於Python 解釋器不是致命的)
NameError 未聲明/初始化對象 (沒有屬性)
UnboundLocalError 訪問未初始化的本地變量
ReferenceError 弱引用(Weak reference)試圖訪問已經垃圾回收了的對象
RuntimeError 通常的運行時錯誤
NotImplementedError 還沒有實現的方法
SyntaxError Python 語法錯誤
IndentationError 縮進錯誤
TabError Tab 和空格混用
SystemError 通常的解釋器系統錯誤
TypeError 對類型無效的操做
ValueError 傳入無效的參數
UnicodeError Unicode 相關的錯誤
UnicodeDecodeError Unicode 解碼時的錯誤
UnicodeEncodeError Unicode 編碼時錯誤
UnicodeTranslateError Unicode 轉換時錯誤
Warning 警告的基類
DeprecationWarning 關於被棄用的特徵的警告
FutureWarning 關於構造未來語義會有改變的警告
OverflowWarning 舊的關於自動提高爲長整型(long)的警告
PendingDeprecationWarning 關於特性將會被廢棄的警告
RuntimeWarning 可疑的運行時行爲(runtime behavior)的警告
SyntaxWarning 可疑的語法的警告
UserWarning 用戶代碼生成的警告

什麼是異常?

異常便是一個事件,該事件會在程序執行過程當中發生,影響了程序的正常執行。數據庫

通常狀況下,在Python沒法正常處理程序時就會發生一個異常。express

異常是Python對象,表示一個錯誤。編程

當Python腳本發生異常時咱們須要捕獲處理它,不然程序會終止執行。網絡

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: Can't convert 'int' object to str implicitly

 

最後一行的錯誤消息指示發生了什麼事。異常有不一樣的類型,其類型會做爲消息的一部分打印出來:在這個例子中的類型有ZeroDivisionErrorNameErrorTypeError.打印出來的異常類型的字符串就是內置的異常的名稱。這對於全部內置的異常是正確的,可是對於用戶自定義的異常就不必定了(儘管這是很是有用的慣例)。標準異常的名稱都是內置的標識符(不是保留的關鍵字)。eclipse

這一行最後一部分給出了異常的詳細信息和引發異常的緣由。ide

錯誤信息的前面部分以堆棧回溯的形式顯示了異常發生的上下文。一般調用棧裏會包含源代碼的行信息,可是來自標準輸入的源碼不會顯示行信息。函數

內置的異常 列出了內置的異常以及它們的含義。工具

語法錯誤

Python 的語法錯誤或者稱之爲解析錯,是初學者常常碰到的,以下實例

>>> while True print('Hello world')
  File "<stdin>", line 1, in ?
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax

 這個例子中,函數 print() 被檢查到有錯誤,是它前面缺乏了一個冒號(:)。語法分析器指出了出錯的一行,而且在最早找到的錯誤的位置標記了一個小小的’箭頭’。錯誤是由箭頭前面的標記引發的(至少檢測到是這樣的): 在這個例子中,檢測到錯誤發生在函數 print(),由於在它以前缺乏一個冒號(':'文件名和行號會一併輸出,因此若是運行的是一個腳本你就知道去哪裏檢查錯誤了。

在程序運行過程當中,總會遇到各類各樣的錯誤。

有的錯誤是程序編寫有問題形成的,好比原本應該輸出整數結果輸出了字符串,這種錯誤咱們一般稱之爲bug,bug是必須修復的。

有的錯誤是用戶輸入形成的,好比讓用戶輸入email地址,結果獲得一個空字符串,這種錯誤能夠經過檢查用戶輸入來作相應的處理。

還有一類錯誤是徹底沒法在程序運行過程當中預測的,好比寫入文件的時候,磁盤滿了,寫不進去了,或者從網絡抓取數據,網絡忽然斷掉了。這類錯誤也稱爲異常,在程序中一般是必須處理的,不然,程序會由於各類問題終止並退出。

Python內置了一套異常處理機制,來幫助咱們進行錯誤處理。

此外,咱們也須要跟蹤程序的執行,查看變量的值是否正確,這個過程稱爲調試。Python的pdb可讓咱們以單步方式執行代碼。

最後,編寫測試也很重要。有了良好的測試,就能夠在程序修改後反覆運行,確保程序輸出符合咱們編寫的測試。

錯誤處理

在程序運行的過程當中,若是發生了錯誤,能夠事先約定返回一個錯誤代碼,這樣,就能夠知道是否有錯,以及出錯的緣由。在操做系統提供的調用中,返回錯誤碼很是常見。好比打開文件的函數 open(),成功時返回文件描述符(就是一個整數),出錯時返回 -1

用錯誤碼來表示是否出錯十分不便,由於函數自己應該返回的正常結果和錯誤碼混在一塊兒,形成調用者必須用大量的代碼來判斷是否出錯:

def foo():
    r = some_function()
    if r==(-1):
        return (-1)
    # do something
    return r

def bar():
    r = foo()
    if r==(-1):
        print('Error')
    else:
        pass

 

一旦出錯,還要一級一級上報,直到某個函數能夠處理該錯誤(好比,給用戶輸出一個錯誤信息)。

因此高級語言一般都內置了一套try...except...finally...的錯誤處理機制,Python也不例外。

try

讓咱們用一個例子來看看try的機制:

try:
    print('try...')
    r = 10 / 0
    print('result:', r)
except ZeroDivisionError as e:
    print('except:', e)
finally:
    print('finally...')
print('END')

 

當咱們認爲某些代碼可能會出錯時,就能夠用try來運行這段代碼,若是執行出錯,則後續代碼不會繼續執行,而是直接跳轉至錯誤處理代碼,即except語句塊,執行完except後,若是有finally語句塊,則執行finally語句塊,至此,執行完畢。

上面的代碼在計算10 / 0時會產生一個除法運算錯誤:

try...
except: division by zero
finally...
END

 

從輸出能夠看到,當錯誤發生時,後續語句print('result:', r)不會被執行,except因爲捕獲到ZeroDivisionError,所以被執行。最後,finally語句被執行。而後,程序繼續按照流程往下走。

若是把除數0改爲2,則執行結果以下:

try...
result: 5
finally...
END

 

因爲沒有錯誤發生,因此except語句塊不會被執行,可是finally若是有,則必定會被執行(能夠沒有finally語句)。

你還能夠猜想,錯誤應該有不少種類,若是發生了不一樣類型的錯誤,應該由不一樣的except語句塊處理。沒錯,能夠有多個except來捕獲不一樣類型的錯誤:

try:
    print('try...')
    r = 10 / int('a')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')

 

int()函數可能會拋出ValueError,因此咱們用一個except捕獲ValueError,用另外一個except捕獲ZeroDivisionError

此外,若是沒有錯誤發生,能夠在except語句塊後面加一個else,當沒有錯誤發生時,會自動執行else語句:

try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')
print('END')

 

Python的錯誤其實也是class,全部的錯誤類型都繼承自BaseException,因此在使用except時須要注意的是,它不但捕獲該類型的錯誤,還把其子類也「一網打盡」。好比:

try:
    foo()
except ValueError as e:
    print('ValueError')
except UnicodeError as e:
    print('UnicodeError')

 

第二個except永遠也捕獲不到UnicodeError,由於UnicodeErrorValueError的子類,若是有,也被第一個except給捕獲了。

Python全部的錯誤都是從BaseException類派生的,常見的錯誤類型和繼承關係看這裏:

https://docs.python.org/3/library/exceptions.html#exception-hierarchy

使用try...except捕獲錯誤還有一個巨大的好處,就是能夠跨越多層調用,好比函數main()調用foo()foo()調用bar(),結果bar()出錯了,這時,只要main()捕獲到了,就能夠處理:

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')

 

也就是說,不須要在每一個可能出錯的地方去捕獲錯誤,只要在合適的層次去捕獲錯誤就能夠了。這樣一來,就大大減小了寫try...except...finally的麻煩。

異常處理

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

try/except語句用來檢測try語句塊中的錯誤,從而讓except語句捕獲異常信息並處理。

若是你不想在異常發生時結束你的程序,只需在try裏捕獲它。

語法:

如下爲簡單的try....except...else的語法:

try:
<語句>        #運行別的代碼
except <名字>:
<語句>        #若是在try部份引起了'name'異常
except <名字>,<數據>:
<語句>        #若是引起了'name'異常,得到附加的數據
else:
<語句>        #若是沒有異常發生

try 語句按如下方式工做。

  • 首先,執行 try 子句tryexcept 關鍵字之間的語句)。
  • 若是未發生任何異常,忽略 except 子句try 語句執行完畢。
  • 若是在 try 子句執行過程當中發生異常,跳過該子句的其他部分。若是異常的類型與 except 關鍵字後面的異常名匹配, 則執行 except 子句,而後繼續執行 try 語句以後的代碼。
  • 若是異常的類型與 except 關鍵字後面的異常名不匹配,它將被傳遞給上層的 try 語句;若是沒有找處處理這個異常的代碼,它就成爲一個 未處理異常 ,程序會終止運行並顯示一條如上所示的信息。

try 語句可能有多個子句,以指定不一樣的異常處理程序。不過至多隻有一個處理程序將被執行。處理程序只處理髮生在相應 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

 try ...except 語句有一個可選的 else 子句 ,其出現時,必須放在全部 except 子句的後面。若是須要在 try 語句沒有拋出異常時執行一些代碼,可使用這個子句。例如:

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 子句中要好,由於它能夠避免意外捕獲不是由 try ... except 語句保護的代碼所引起的異常。

當異常發生時,它可能帶有相關數據,也稱爲異常的參數參數的有無和類型取決於異常的類型。

except 子句能夠在異常名以後指定一個變量。這個變量將綁定於一個異常實例,同時異常的參數將存放在 實例的args 中。爲方便起見,異常實例定義了 __str__() ,所以異常的參數能夠直接打印而沒必要引用 .args也能夠在引起異常以前先實例化一個異常,而後向它添加任何想要的屬性。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

 

對於未處理的異常,若是它含有參數,那麼參數會做爲異常信息的最後一部分打印出來。

異常處理程序不只處理直接發生在 try 子句中的異常,並且還處理 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

 

拋出異常

能夠經過編程來選擇處理部分異常。看一下下面的例子,它會一直要求用戶輸入直到輸入一個合法的整數爲止,但容許用戶中斷這個程序(使用 Control-C 或系統支持的任何方法);注意用戶產生的中斷引起的是 KeyboardInterrupt 異常。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

 

Python 使用 raise 語句拋出一個指定的異常。例如:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: HiThere

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

調用堆棧

若是錯誤沒有被捕獲,它就會一直往上拋,最後被Python解釋器捕獲,打印一個錯誤信息,而後程序退出。來看看err.py

# err.py:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()

 

執行,結果以下:

$ python3 err.py
Traceback (most recent call last):
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero

 

出錯並不可怕,可怕的是不知道哪裏出錯了。解讀錯誤信息是定位錯誤的關鍵。咱們從上往下能夠看到整個錯誤的調用函數鏈:

錯誤信息第1行:

Traceback (most recent call last):

 

告訴咱們這是錯誤的跟蹤信息。

第2~3行:

File "err.py", line 11, in <module>
    main()

 

調用main()出錯了,在代碼文件err.py的第11行代碼,但緣由是第9行:

 File "err.py", line 9, in main
    bar('0')

 

調用bar('0')出錯了,在代碼文件err.py的第9行代碼,但緣由是第6行:

File "err.py", line 6, in bar
    return foo(s) * 2

 

緣由是return foo(s) * 2這個語句出錯了,但這還不是最終緣由,繼續往下看:

File "err.py", line 3, in foo
    return 10 / int(s)

 

緣由是return 10 / int(s)這個語句出錯了,這是錯誤產生的源頭,由於下面打印了:

ZeroDivisionError: integer division or modulo by zero

 

根據錯誤類型ZeroDivisionError,咱們判斷,int(s)自己並無出錯,可是int(s)返回0,在計算10 / 0時出錯,至此,找到錯誤源頭。

記錄錯誤

若是不捕獲錯誤,天然可讓Python解釋器來打印出錯誤堆棧,但程序也被結束了。既然咱們能捕獲錯誤,就能夠把錯誤堆棧打印出來,而後分析錯誤緣由,同時,讓程序繼續執行下去。

Python內置的logging模塊能夠很是容易地記錄錯誤信息:

# err_logging.py

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

 

一樣是出錯,但程序打印完錯誤信息後會繼續執行,並正常退出:

$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
  File "err_logging.py", line 13, in main
    bar('0')
  File "err_logging.py", line 9, in bar
    return foo(s) * 2
  File "err_logging.py", line 6, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END

 

經過配置,logging還能夠把錯誤記錄到日誌文件裏,方便過後排查。

拋出錯誤

由於錯誤是class,捕獲一個錯誤就是捕獲到該class的一個實例。所以,錯誤並非憑空產生的,而是有意建立並拋出的。Python的內置函數會拋出不少類型的錯誤,咱們本身編寫的函數也能夠拋出錯誤。

若是要拋出錯誤,首先根據須要,能夠定義一個錯誤的class,選擇好繼承關係,而後,用raise語句拋出一個錯誤的實例:

# err_raise.py
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

foo('0')

 

執行,能夠最後跟蹤到咱們本身定義的錯誤:

$ python3 err_raise.py 
Traceback (most recent call last):
  File "err_throw.py", line 11, in <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

 

只有在必要的時候才定義咱們本身的錯誤類型。若是能夠選擇Python已有的內置的錯誤類型(好比ValueErrorTypeError),儘可能使用Python內置的錯誤類型。

最後,咱們來看另外一種錯誤處理的方式:

# err_reraise.py

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()

 

bar()函數中,咱們明明已經捕獲了錯誤,可是,打印一個ValueError!後,又把錯誤經過raise語句拋出去了,這不有病麼?

其實這種錯誤處理方式不但沒病,並且至關常見。捕獲錯誤目的只是記錄一下,便於後續追蹤。可是,因爲當前函數不知道應該怎麼處理該錯誤,因此,最恰當的方式是繼續往上拋,讓頂層調用者去處理。比如一個員工處理不了一個問題時,就把問題拋給他的老闆,若是他的老闆也處理不了,就一直往上拋,最終會拋給CEO去處理。

raise語句若是不帶參數,就會把當前錯誤原樣拋出。此外,在exceptraise一個Error,還能夠把一種類型的錯誤轉化成另外一種類型:

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

 

只要是合理的轉換邏輯就能夠,可是,決不該該把一個IOError轉換成絕不相干的ValueError

小結

Python內置的try...except...finally用來處理錯誤十分方便。出錯時,會分析錯誤信息並定位錯誤發生的代碼位置纔是最關鍵的。

程序也能夠主動拋出錯誤,讓調用者來處理相應的錯誤。可是,應該在文檔中寫清楚可能會拋出哪些錯誤,以及錯誤產生的緣由。

用戶自定義異常

程序能夠經過建立新的異常類來命名本身的異常(Python 類的更多內容請參見 )。異常一般應該繼承 Exception 類,直接繼承或者間接繼承均可以。

異常類能夠像其餘類同樣作任何事情,可是一般都會比較簡單,只提供一些屬性以容許異常處理程序獲取錯誤相關的信息。建立一個可以引起幾種不一樣錯誤的模塊時,一個一般的作法是爲該模塊定義的異常建立一個基類,而後基於這個基類爲不一樣的錯誤狀況建立特定的子類:

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

 無論有沒有發生異常,在離開 try 語句以前老是會執行 finally 子句。try 子句中發生了一個異常,而且沒有 except 字句處理(或者異常發生在 exceptelse 子句中),在執行完 finally 子句後將從新引起這個異常。try 語句因爲 breakcontinereturn 語句離開時,一樣會執行finally 子句。下面是一個更復雜些的例子:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

正如您所看到的,在任何狀況下都會執行 finally 子句。由兩個字符串相除引起的 TypeError 異常沒有被 except子句處理,所以在執行 finally 子句後被從新引起。

在真實的應用程序中, finally 子句用於釋放外部資源(例如文件或網絡鏈接),無論資源的使用是否成功。

 

 

大多數異常的名字都以"Error"結尾,相似於標準異常的命名。

不少標準模塊中都定義了本身的異常來報告在它們所定義的函數中可能發生的錯誤。 這一章給出了類的詳細信息。

 

>>> 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__() 被覆蓋。



定義清理行爲

try 語句還有另一個可選的子句,它定義了不管在任何狀況下都會執行的清理行爲。 例如:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in ?

 以上例子無論try子句裏面有沒有發生異常,finally子句都會執行。

若是一個異常在 try 子句裏(或者在 except 和 else 子句裏)被拋出,而又沒有任何的 except 把它截住,那麼這個異常會在 finally 子句執行後再次被拋出。

下面是一個更加複雜的例子(在同一個 try 語句裏包含 except 和 finally 子句):

>>> def divide(x, y):
        try:
            result = x / y
        except ZeroDivisionError:
            print("division by zero!")
        else:
            print("result is", result)
        finally:
            print("executing finally clause")
   
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

 

 

預約義的清理行爲

一些對象定義了標準的清理行爲,不管系統是否成功的使用了它,一旦不須要它了,那麼這個標準的清理行爲就會執行。

這面這個例子展現了嘗試打開一個文件,而後把內容打印到屏幕上:

for line in open("myfile.txt"):
    print(line, end="")

以上這段代碼的問題是,當執行完畢後,文件會保持打開狀態,並無被關閉。

關鍵詞 with 語句就能夠保證諸如文件之類的對象在使用完以後必定會正確的執行他的清理方法:

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

執行該語句後,文件 f 將始終被關閉,即便在處理某一行時遇到了問題。提供預約義的清理行爲的對象,和文件同樣,會在它們的文檔裏說明。

調試

程序能一次寫完並正常運行的機率很小,基本不超過1%。總會有各類各樣的bug須要修正。有的bug很簡單,看看錯誤信息就知道,有的bug很複雜,咱們須要知道出錯時,哪些變量的值是正確的,哪些變量的值是錯誤的,所以,須要一整套調試程序的手段來修復bug。

第一種方法簡單直接粗暴有效,就是用print()把可能有問題的變量打印出來看看:

def foo(s):
    n = int(s)
    print('>>> n = %d' % n)
    return 10 / n

def main():
    foo('0')

main()

 

執行後在輸出中查找打印的變量值:

$ python3 err.py
>>> n = 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero

 

print()最大的壞處是未來還得刪掉它,想一想程序裏處處都是print(),運行結果也會包含不少垃圾信息。因此,咱們又有第二種方法。

斷言

凡是用print()來輔助查看的地方,均可以用斷言(assert)來替代:

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

 assert的意思是,表達式n != 0應該是True,不然,根據程序運行的邏輯,後面的代碼確定會出錯。

若是斷言失敗,assert語句自己就會拋出AssertionError

$ python3 err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

 程序中若是處處充斥着assert,和print()相比也好不到哪去。不過,啓動Python解釋器時能夠用-O參數來關閉assert

$ python3 -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

 關閉後,你能夠把全部的assert語句當成pass來看。

logging

print()替換爲logging是第3種方式,和assert比,logging不會拋出錯誤,並且能夠輸出到文件:

import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

 

logging.info()就能夠輸出一段文本。運行,發現除了ZeroDivisionError,沒有任何信息。怎麼回事?

別急,在import logging以後添加一行配置再試試:

import logging
logging.basicConfig(level=logging.INFO)

 

看到輸出了:

$ python3 err.py
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

 

這就是logging的好處,它容許你指定記錄信息的級別,有debuginfowarningerror等幾個級別,當咱們指定level=INFO時,logging.debug就不起做用了。同理,指定level=WARNING後,debuginfo就不起做用了。這樣一來,你能夠放心地輸出不一樣級別的信息,也不用刪除,最後統一控制輸出哪一個級別的信息。

logging的另外一個好處是經過簡單的配置,一條語句能夠同時輸出到不一樣的地方,好比console和文件。

pdb

第4種方式是啓動Python的調試器pdb,讓程序以單步方式運行,能夠隨時查看運行狀態。咱們先準備好程序:

# err.py
s = '0'
n = int(s)
print(10 / n)

 

而後啓動:

$ python3 -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'

 

以參數-m pdb啓動後,pdb定位到下一步要執行的代碼-> s = '0'。輸入命令l來查看代碼:

(Pdb) l
  1     # err.py
  2  -> s = '0'
  3     n = int(s)
  4     print(10 / n)

輸入命令n能夠單步執行代碼:

(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)

 

任什麼時候候均可以輸入命令p 變量名來查看變量:

(Pdb) p s
'0'
(Pdb) p n
0

 

輸入命令q結束調試,退出程序:

(Pdb) q

 

這種經過pdb在命令行調試的方法理論上是萬能的,但實在是太麻煩了,若是有一千行代碼,要運行到第999行得敲多少命令啊。還好,咱們還有另外一種調試方法。

pdb.set_trace()

這個方法也是用pdb,可是不須要單步執行,咱們只須要import pdb,而後,在可能出錯的地方放一個pdb.set_trace(),就能夠設置一個斷點:

# err.py
import pdb

s = '0'
n = int(s)
pdb.set_trace() # 運行到這裏會自動暫停
print(10 / n)

 

運行代碼,程序會自動在pdb.set_trace()暫停並進入pdb調試環境,能夠用命令p查看變量,或者用命令c繼續運行:

$ python3 err.py 
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
  File "err.py", line 7, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

 

這個方式比直接啓動pdb單步調試效率要高不少,但也高不到哪去。

IDE

若是要比較爽地設置斷點、單步執行,就須要一個支持調試功能的IDE。目前比較好的Python IDE有PyCharm:

http://www.jetbrains.com/pycharm/

另外,Eclipse加上pydev插件也能夠調試Python程序。

小結

寫程序最痛苦的事情莫過於調試,程序每每會以你意想不到的流程來運行,你期待執行的語句其實根本沒有執行,這時候,就須要調試了。

雖然用IDE調試起來比較方便,可是最後你會發現,logging纔是終極武器。

單元測試

若是你據說過「測試驅動開發」(TDD:Test-Driven Development),單元測試就不陌生。

單元測試是用來對一個模塊、一個函數或者一個類來進行正確性檢驗的測試工做。

好比對函數abs(),咱們能夠編寫出如下幾個測試用例:

  1. 輸入正數,好比11.20.99,期待返回值與輸入相同;

  2. 輸入負數,好比-1-1.2-0.99,期待返回值與輸入相反;

  3. 輸入0,期待返回0

  4. 輸入非數值類型,好比None[]{},期待拋出TypeError

把上面的測試用例放到一個測試模塊裏,就是一個完整的單元測試。

若是單元測試經過,說明咱們測試的這個函數可以正常工做。若是單元測試不經過,要麼函數有bug,要麼測試條件輸入不正確,總之,須要修復使單元測試可以經過。

單元測試經過後有什麼意義呢?若是咱們對abs()函數代碼作了修改,只須要再跑一遍單元測試,若是經過,說明咱們的修改不會對abs()函數原有的行爲形成影響,若是測試不經過,說明咱們的修改與原有行爲不一致,要麼修改代碼,要麼修改測試。

這種以測試爲驅動的開發模式最大的好處就是確保一個程序模塊的行爲符合咱們設計的測試用例。在未來修改的時候,能夠極大程度地保證該模塊行爲仍然是正確的。

咱們來編寫一個Dict類,這個類的行爲和dict一致,可是能夠經過屬性來訪問,用起來就像下面這樣:

>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

 

mydict.py代碼以下:

class Dict(dict):

    def __init__(self, **kw):
        super().__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

 

爲了編寫單元測試,咱們須要引入Python自帶的unittest模塊,編寫mydict_test.py以下:

import unittest

from mydict import Dict

class TestDict(unittest.TestCase):

    def test_init(self):
        d = Dict(a=1, b='test')
        self.assertEqual(d.a, 1)
        self.assertEqual(d.b, 'test')
        self.assertTrue(isinstance(d, dict))

    def test_key(self):
        d = Dict()
        d['key'] = 'value'
        self.assertEqual(d.key, 'value')

    def test_attr(self):
        d = Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEqual(d['key'], 'value')

    def test_keyerror(self):
        d = Dict()
        with self.assertRaises(KeyError):
            value = d['empty']

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty

 

編寫單元測試時,咱們須要編寫一個測試類,從unittest.TestCase繼承。

test開頭的方法就是測試方法,不以test開頭的方法不被認爲是測試方法,測試的時候不會被執行。

對每一類測試都須要編寫一個test_xxx()方法。因爲unittest.TestCase提供了不少內置的條件判斷,咱們只須要調用這些方法就能夠斷言輸出是不是咱們所指望的。最經常使用的斷言就是

assertEqual():

self.assertEqual(abs(-1), 1) # 斷言函數返回的結果與1相等

 

另外一種重要的斷言就是期待拋出指定類型的Error,好比經過d['empty']訪問不存在的key時,斷言會拋出KeyError

with self.assertRaises(KeyError):
    value = d['empty']

 

而經過d.empty訪問不存在的key時,咱們期待拋出AttributeError

with self.assertRaises(AttributeError):
    value = d.empty

 

運行單元測試

一旦編寫好單元測試,咱們就能夠運行單元測試。最簡單的運行方式是在mydict_test.py的最後加上兩行代碼:

if __name__ == '__main__':
    unittest.main()

 這樣就能夠把mydict_test.py當作正常的python腳本運行:

$ python3 mydict_test.py

 另外一種方法是在命令行經過參數-m unittest直接運行單元測試:

$ python3 -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

 

這是推薦的作法,由於這樣能夠一次批量運行不少單元測試,而且,有不少工具能夠自動來運行這些單元測試。

setUp與tearDown

能夠在單元測試中編寫兩個特殊的setUp()tearDown()方法。這兩個方法會分別在每調用一個測試方法的先後分別被執行。

setUp()tearDown()方法有什麼用呢?設想你的測試須要啓動一個數據庫,這時,就能夠在setUp()方法中鏈接數據庫,在tearDown()方法中關閉數據庫,這樣,沒必要在每一個測試方法中重複相同的代碼:

class TestDict(unittest.TestCase):

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...')

 

能夠再次運行測試看看每一個測試方法調用先後是否會打印出setUp...tearDown...

小結

單元測試能夠有效地測試某個程序模塊的行爲,是將來重構代碼的信心保證。

單元測試的測試用例要覆蓋經常使用的輸入組合、邊界條件和異常。

單元測試代碼要很是簡單,若是測試代碼太複雜,那麼測試代碼自己就可能有bug。

單元測試經過了並不意味着程序就沒有bug了,可是不經過程序確定有bug。

文檔測試

若是你常常閱讀Python的官方文檔,能夠看到不少文檔都有示例代碼。好比 re模塊就帶了不少示例代碼:
>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'

 

能夠把這些示例代碼在Python的交互式環境下輸入並執行,結果與文檔中的示例代碼顯示的一致。

這些代碼與其餘說明能夠寫在註釋中,而後,由一些工具來自動生成文檔。既然這些代碼自己就能夠粘貼出來直接運行,那麼,可不能夠自動執行寫在註釋中的這些代碼呢?

答案是確定的。

當咱們編寫註釋時,若是寫上這樣的註釋:

def abs(n):
    '''
    Function to get absolute value of number.

    Example:

    >>> abs(1)
    1
    >>> abs(-1)
    1
    >>> abs(0)
    0
    '''
    return n if n >= 0 else (-n)

 無疑更明確地告訴函數的調用者該函數的指望輸入和輸出。

而且,Python內置的「文檔測試」(doctest)模塊能夠直接提取註釋中的代碼並執行測試。

doctest嚴格按照Python交互式命令行的輸入和輸出來判斷測試結果是否正確。只有測試異常的時候,能夠用...表示中間一大段煩人的輸出。

讓咱們用doctest來測試上次編寫的Dict類:

# mydict2.py
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__':
    import doctest
    doctest.testmod()

 

運行python3 mydict2.py

$ python3 mydict2.py

 

什麼輸出也沒有。這說明咱們編寫的doctest運行都是正確的。若是程序有問題,好比把__getattr__()方法註釋掉,再運行就會報錯:

$ python3 mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
    d1.x
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
    d2.c
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
   2 of   9 in __main__.Dict
***Test Failed*** 2 failures.

 

注意到最後3行代碼。當模塊正常導入時,doctest不會被執行。只有在命令行直接運行時,才執行doctest。因此,沒必要擔憂doctest會在非測試環境下執行。

相關文章
相關標籤/搜索