Python異常(try...except)對程式碼執行效能的影響
前言
Python的異常處理能力非常強大,但是用不好也會帶來負面的影響。我平時寫程式的過程中也喜歡使用異常,雖然採取防禦性的方式編碼會更好,但是交給異常處理會起到偷懶作用。偶爾會想想異常處理會對效能造成多大的影響,於是今天就試著測試了一下。
Python異常(谷歌開源風格指南)
tip:
允許使用異常, 但必須小心。
定義:
異常是一種跳出程式碼塊的正常控制流來處理錯誤或者其它異常條件的方式。
優點:
正常操作程式碼的控制流不會和錯誤處理程式碼混在一起. 當某種條件發生時, 它也允許控制流跳過多個框架. 例如, 一步跳出N個巢狀的函式, 而不必繼續執行錯誤的程式碼。
缺點:
可能會導致讓人困惑的控制流. 呼叫庫時容易錯過錯誤情況。
結論:
異常必須遵守特定條件:
- 像這樣觸發異常:
raise MyException("Error message")
或者raise MyException
. 不要使用兩個引數的形式(raise MyException, "Error message"
)或者過時的字串異常(raise "Error message"
)。 - 模組或包應該定義自己的特定域的異常基類, 這個基類應該從內建的Exception類繼承. 模組的異常基類應該叫做”Error”。
class Error(Exception) :
pass
- 永遠不要使用
except:
語句來捕獲所有異常, 也不要捕獲Exception
或者StandardError
, 除非你打算重新觸發該異常, 或者你已經在當前執行緒的最外層(記得還是要列印一條錯誤訊息). 在異常這方面, Python非常寬容,except:
真的會捕獲包括Python語法錯誤在內的任何錯誤. 使用except:
很容易隱藏真正的bug。 - 儘量減少try/except塊中的程式碼量. try塊的體積越大, 期望之外的異常就越容易被觸發. 這種情況下, try/except塊將隱藏真正的錯誤。
- 使用finally子句來執行那些無論try塊中有沒有異常都應該被執行的程式碼. 這對於清理資源常常很有用, 例如關閉檔案。
- 當捕獲異常時, 使用
as
而不要用逗號. 例如
try:
raise Error
except Error as error:
pass
設計實驗方式
採取比較簡單直觀的對照實驗。
先定義一個裝飾器,用來計算每個函式執行所需時間:
def timer(func):
import time
def wrapper(*args, **kwargs):
startTime = time.time()
f = func(*args, **kwargs)
endTime = time.time()
passTime = endTime - startTime
print "執行函式%s使用了%f秒" % (getattr(func, "__name__"), passTime)
return f
return wrapper
然後用該裝飾器裝飾測試的函式即可。
再定義一個叫do_something的函式,這個函式中就做一件事,把1賦值給變數a。在每個測試函式中,都會呼叫這個函式1000000次。
do_something:
def do_something():
a = 1
我根據情況設計了不同的測試組:
測試組1(直接執行耗時操作):
@timer
def test1():
for _ in xrange(1000000):
do_something()
測試組2(耗時操作放在try中執行,不丟擲錯誤):
@timer
def test2():
try:
for _ in xrange(1000000):
do_something()
except Exception:
do_something()
else:
pass
finally:
pass
測試組3(try放耗時操作中,try每一次操作,不丟擲錯誤):
@timer
def test3():
for _ in xrange(1000000):
try:
do_something()
except Exception:
do_something()
else:
pass
finally:
pass
測試組4(try放耗時操作中,try每一次操作並進行異常處理(捕捉丟擲的特定異常)):
@timer
def test4():
zero = 0
for _ in xrange(1000000):
try:
if zero == 0:
raise ZeroDivisionError
except ZeroDivisionError:
do_something()
else:
pass
finally:
pass
測試組5(try放耗時操作中,try每一次操作並進行異常處理(捕捉所有異常 try…except BaseException)):
@timer
def test5():
zero = 0
for _ in xrange(1000000):
try:
if zero == 0:
raise ZeroDivisionError
except BaseException:
do_something()
else:
pass
finally:
pass
測試組6(try放耗時操作中,try每一次操作並進行異常處理(捕捉所有異常 不帶任何異常型別)):
@timer
def test6():
zero = 0
for _ in xrange(1000000):
try:
if zero == 0:
raise ZeroDivisionError
except:
do_something()
else:
pass
finally:
pass
測試組7(耗時操作放在except中):
@timer
def test7():
zero = 0
try:
if zero == 0:
raise ZeroDivisionError
except ZeroDivisionError:
for _ in xrange(1000000):
do_something()
else:
pass
finally:
pass
測試組8(防禦式編碼):
@timer
def test8():
zero = 0
for _ in xrange(1000000):
if zero == 0:
do_something()
執行結果
對比結論
- 通過對比1和2,可以得知直接執行耗時操作和耗時操作放在try中執行並無異常觸發時效能消耗幾乎是一樣的。
- 通過對比2和7,可以得知使用異常的使用無論是把程式碼放在 try 中執行還是在 except 中執行效能消耗幾乎是一樣的。
- 通過對比2和3,可以得知當不丟擲錯誤時,把try放耗時操作中比耗時操作放在try中效能消耗要略大。
- 通過對比3和4,可以得知當使用try時無異常丟擲跟使用try時丟擲異常效能消耗幾乎相差好幾倍。
- 通過對比4和5,可以得知try放耗時操作中時,try每一次操作並進行異常處理(捕捉丟擲的特定異常)跟try每一次操作並進行異常處理(捕捉所有異常 try…except BaseException)效能消耗幾乎是一樣的。
- 通過對比4和8,可以得知使用防禦性方式編碼比捕捉異常方式效能消耗幾乎相差好幾倍。
- 通過對比5和6,可以得知捕捉所有異常(try…except)方式比捕捉所有異常(try…except BaseException)方式要略快。
總結
由以上對比結論,可以總結為:
無論是把程式碼放在 try 中執行還是在 except 中執行效能消耗幾乎是一樣的。
直接執行程式碼與放在try中執行且不丟擲異常時效能消耗幾乎是一樣的,當然理論上try會消耗一點效能,可以忽略不計。
雖然try…except的方式比try…except BaseException和捕捉丟擲的特定異常的方式要略快,但扔不建議採取這種方式,因為前者很容易隱藏真正的bug,從而帶來嚴重後果。
通常要採取捕捉丟擲的特定異常而不是捕捉所有異常,雖然二者效能消耗幾乎一樣。
防禦性方式編碼比捕捉異常方式效能消耗幾乎相差好幾倍,應儘量採取這種程式設計方式,提升效能並且更靠譜。