- 1 錯誤和異常
- 1.1 簡介
- 1.1.1 語法錯誤
- 1.1.2 異常
- 1.2 丟擲異常
- 1.2.1 丟擲原裝異常
- 1.2.2 assert異常
- 1.2.3 丟擲使用者自定義異常
- 1.3 異常處理
- 1.3.1 try/except
- 1.3.2 try/except...else
- 1.3.3 try-finally 語句
- 1.3.4 with 關鍵字
- 1.4 分析記錄錯誤
- 1.4.1 分析錯誤
- 1.4.2 記錄錯誤
- 1.5 單元測試
- 1.5.1 unittest單元測試
- 1.5.2 setUp與tearDown
- 1.5.3 文件測試
- 1.1 簡介
1 錯誤和異常
1.1 簡介
Python 有兩種錯誤很容易辨認:語法錯誤
和異常
Python assert
(斷言)用於判斷一個表示式,在表示式條件為 false 的時候觸發異常。
1.1.1 語法錯誤
Python 的語法錯誤或者稱之為解析錯,是初學者經常碰到的,如下例項
>>> while True print('Hello world')
File "<stdin>", line 1, in ?
while True print('Hello world')
^
SyntaxError: invalid syntax
這個例子中,函式 print() 被檢查到有錯誤,是它前面缺少了一個冒號 : 。
語法分析器指出了出錯的一行,並且在最先找到的錯誤的位置標記了一個小小的箭頭。
1.1.2 異常
即便 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 <module>
TypeError: can only concatenate str (not "int") to str
異常以不同的型別出現,這些型別都作為資訊的一部分列印出來: 例子中的型別有 ZeroDivisionError,NameError 和 TypeError。
錯誤資訊的前面部分顯示了異常發生的上下文,並以呼叫棧的形式顯示具體資訊。
1.2 丟擲異常
1.2.1 丟擲原裝異常
Python 使用 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
1.2.2 assert異常
Python assert
(斷言)用於判斷一個表示式,在表示式條件為 false
的時候觸發異常。
斷言
可以在條件不滿足程式執行的情況下直接返回錯誤,而不必等待程式執行後出現崩潰的情況,例如我們的程式碼只能在 Linux 系統下執行,可以先判斷當前系統是否符合條件。
語法格式如下:assert expression
或者附帶引數assert expression [, arguments]
等價於:
if not expression:
raise AssertionError
或者帶引數
if not expression:
raise AssertionError(arguments)
以下為 assert 使用例項:
>>> assert True # 條件為 true 正常執行
>>> assert False # 條件為 false 觸發異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1==1 # 條件為 true 正常執行
>>> assert 1==2 # 條件為 false 觸發異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1==2, '1 不等於 2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: 1 不等於 2
>>>
以下例項判斷當前系統是否為 Linux,如果不滿足條件則直接觸發異常,不必執行接下來的程式碼:
import sys
assert ('linux' in sys.platform), "該程式碼只能在 Linux 下執行"
1.2.3 丟擲使用者自定義異常
可以透過建立一個新的異常類來擁有自己的異常。異常類繼承自 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
大多數的異常的名字都以"Error"結尾,就跟標準的異常命名一樣。
1.3 異常處理
1.3.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 子句將被執行。 - 如果一個異常沒有與任何的 except 匹配,那麼這個異常將會傳遞給上層的 try 中。
一個 try
語句可能包含多個except
子句,分別來處理不同的特定的異常。最多隻有一個分支會被執行。
處理程式將只針對對應的 try 子句中的異常進行處理,而不是其他的 try 的處理程式中的異常。
一個except
子句可以同時處理多個異常,這些異常將被放在一個括號裡成為一個元組,例如:
except (RuntimeError, TypeError, NameError):
pass
最後一個except
子句可以忽略異常的名稱,它將被當作萬用字元使用。可以使用這種方法列印一個錯誤資訊,然後使用raise
再次把異常丟擲。
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
1.3.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
1.3.3 try-finally 語句
try-finally
語句無論是否發生異常都將執行最後的程式碼。
以下例項中 finally 語句無論異常是否發生都會執行:
try:
runoob()
except AssertionError as error:
print(error)
else:
try:
with open('file.log') as file:
read_data = file.read()
except FileNotFoundError as fnf_error:
print(fnf_error)
finally:
print('這句話,無論異常是否發生都會執行。')
下面是一個更加複雜的例子(在同一個 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'
1.3.4 with 關鍵字
Python 中的 with
語句用於異常處理,封裝了 try…except…finally
編碼正規化,提高了易用性。
with
語句使程式碼更清晰、更具可讀性, 它簡化了檔案流等公共資源的管理。
在處理檔案物件時使用 with
關鍵字是一種很好的做法。
我們可以看下以下幾種程式碼例項:不使用 with,也不使用 try…except…finally
file = open('./test_runoob.txt', 'w')
file.write('hello world !')
file.close()
以上程式碼如果在呼叫 write 的過程中,出現了異常,則 close
方法將無法被執行,因此資源就會一直被該程式佔用而無法被釋放。 接下來我們呢可以使用 try…except…finally
來改進程式碼:
file = open('./test_runoob.txt', 'w')
try:
file.write('hello world')
finally:
file.close()
以上程式碼我們對可能發生異常的程式碼處進行 try 捕獲,發生異常時執行 except 程式碼塊,finally 程式碼塊是無論什麼情況都會執行,所以檔案會被關閉,不會因為執行異常而佔用資源。
使用 with 關鍵字:
with open('./test_runoob.txt', 'w') as file:
file.write('hello world !')
使用 with
關鍵字系統會自動呼叫 f.close()
方法, with
的作用等效於 try/finally
語句是一樣的。
我們可以在執行 with 關鍵字後檢驗檔案是否關閉:
>>> with open('./test_runoob.txt') as f:
... read_data = f.read()
>>> # 檢視檔案是否關閉
>>> f.closed
True
with 語句實現原理建立在上下文管理器之上。上下文管理器是一個實現 __enter__
和 __exit__
方法的類。使用 with
語句確保在巢狀塊的末尾呼叫 __exit__
方法。這個概念類似於 try...finally 塊的使用。
with open('./test_runoob.txt', 'w') as my_file:
my_file.write('hello world!')
以上例項將 hello world! 寫到 ./test_runoob.txt 檔案上。
在檔案物件中定義了 __enter__
和 __exit__
方法,即檔案物件也實現了上下文管理器,首先呼叫 __enter__
方法,然後執行 with
語句中的程式碼,最後呼叫 __exit__
方法。 即使出現錯誤,也會呼叫 __exit__
方法,也就是會關閉檔案流。
1.4 分析記錄錯誤
1.4.1 分析錯誤
如果錯誤沒有被捕獲,它就會一直往上拋,最後被Python直譯器捕獲,列印一個錯誤資訊,然後程式退出。來看看err.py:
# err.py:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
執行,結果如下:
$ python 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: integer division or modulo by zero
出錯並不可怕,可怕的是不知道哪裡出錯了。解讀錯誤資訊是定位錯誤的關鍵。我們從上往下可以看到整個錯誤的呼叫函式鏈:
錯誤資訊第1行:
Traceback (most recent call last):
告訴我們這是錯誤的跟蹤資訊。
第2行:
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時出錯,至此,找到錯誤源頭。
1.4.2 記錄錯誤
如果不捕獲錯誤,自然可以讓Python直譯器
來列印出錯誤堆疊,但程式也被結束了。既然我們能捕獲錯誤,就可以把錯誤堆疊列印出來,然後分析錯誤原因,同時,讓程式繼續執行下去。
Python內建的logging模組
可以非常容易地記錄錯誤資訊:
# err.py
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except StandardError, e:
logging.exception(e)
main()
print 'END'
同樣是出錯,但程式列印完錯誤資訊後會繼續執行,並正常退出:
$ python err.py
ERROR:root:integer division or modulo by zero
Traceback (most recent call last):
File "err.py", line 12, in main
bar('0')
File "err.py", line 8, in bar
return foo(s) * 2
File "err.py", line 5, in foo
return 10 / int(s)
ZeroDivisionError: integer division or modulo by zero
END
透過配置,logging還可以把錯誤記錄到日誌檔案裡,方便事後排查。
1.5 單元測試
1.5.1 unittest單元測試
為了編寫單元測試,我們需要引入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.assertEquals(d.a, 1)
self.assertEquals(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEquals(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEquals(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
提供了很多內建的條件判斷,我們只需要呼叫這些方法就可以斷言輸出是否是我們所期望的。最常用的斷言就是assertEquals()
:self.assertEquals(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指令碼執行,另一種更常見的方法是在命令列透過引數-m unittest
直接執行單元測試:$ python -m unittest mydict_test
1.5.2 setUp與tearDown
可以在單元測試中編寫兩個特殊的setUp()和tearDown()
方法。這兩個方法會分別在每呼叫一個測試方法的前後分別被執行。
setUp()和tearDown()方法有什麼用呢?設想測試需要啟動一個資料庫,這時,就可以在setUp()
方法中連線資料庫,在tearDown()
方法中關閉資料庫,這樣,不必在每個測試方法中重複相同的程式碼:
class TestDict(unittest.TestCase):
def setUp(self):
print 'setUp...'
def tearDown(self):
print 'tearDown...'
可以再次執行測試看看每個測試方法呼叫前後是否會列印出setUp...和tearDown...。
1.5.3 文件測試
當我們編寫註釋時,如果寫上這樣的註釋:
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類:
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()
執行python mydict.py:
$ python mydict.py
什麼輸出也沒有。這說明我們編寫的doctest執行都是正確的。如果程式有問題,比如把__getattr__()
方法註釋掉,再執行就會報錯:
$ python mydict.py
**********************************************************************
File "mydict.py", line 7, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "mydict.py", line 13, in __main__.Dict
Failed example:
d2.c
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
注意到最後兩行程式碼。當模組正常匯入時,doctest
不會被執行。只有在命令列執行時,才執行doctest
。所以,不必擔心doctest會在非測試環境下執行。