介紹一個自己造的輪子,Python2異常鏈。
需求
習(xí)慣了用java擼碼僵腺,雖說膠水代碼多,但能比較好的用代碼表達(dá)思路壶栋;而Python則簡潔到了簡陋的地步——各種雞肋的語法糖辰如,各種不完善的機(jī)制。比如錯誤處理贵试。
Python2沒有異常鏈琉兜,讓問題排查變得非常困難:
# coding=utf-8
import sys
class UnexpectedError(StandardError):
pass
def divide(division, divided):
if division == 0:
raise ValueError("illegal input: %s, %s" % (division, divided))
ans = division / divided
return ans
a = 0
b = 0
try:
print divide(a, b)
except ValueError as e:
# m_counter.inc("err", 1)
raise UnexpectedError("illegal input: %s, %s" % (a, b))
except ZeroDivisionError as e:
# m_counter.inc("err", 1)
raise UnexpectedError("divide by zero")
except StandardError as e:
# m_counter.inc("err", 1)
raise UnexpectedError("other error...")
打印異常如下:
Traceback (most recent call last):
File "/Users/monkeysayhi/PycharmProjects/Wheel/utils/tmp/tmp.py", line 22, in <module>
raise UnexpectedError("illegal input: %s, %s" % (a, b))
__main__.UnexpectedError: illegal input: 0, 0
不考慮代碼風(fēng)格凯正,是標(biāo)準(zhǔn)的Python2異常處理方式:分別捕獲異常,再統(tǒng)一成一個異常豌蟋,只有msg不同廊散,重新拋出。這種寫法又丑又冗余梧疲,頂多可以改成這樣:
try:
print divide(a, b)
except StandardError as e:
# m_counter.inc("err", 1)
raise UnexpectedError(e.message)
即便如此允睹,也無法解決一個最嚴(yán)重的問題:明明是11行拋出的異常,但打印出來的異常棧卻只能追蹤到22行重新拋出異常的raise
語句幌氮。重點在于沒有記錄cause缭受,使我們追蹤到22行之后,不知道為什么會拋出cause该互,也就無法定位到實際發(fā)生問題的代碼米者。
異常鏈
最理想的方式,還是在異常棧中打印異常鏈:
try:
print divide(a, b)
except StandardError as cause:
# m_counter.inc("err", 1)
raise UnexpectedError("some msg", cause)
就像Java的異常棧宇智,區(qū)分“要拋出的異常UnexpectedError和引起該異常的原因cause”:
java.lang.RuntimeException: level 2 exception
at com.msh.demo.exceptionStack.Test.fun2(Test.java:17)
at com.msh.demo.exceptionStack.Test.main(Test.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.io.IOException: level 1 exception
at com.msh.demo.exceptionStack.Test.fun1(Test.java:10)
at com.msh.demo.exceptionStack.Test.fun2(Test.java:15)
... 6 more
上述異常棧表示蔓搞,RuntimeException由IOException導(dǎo)致;1行與9行下是各異常的調(diào)用路徑trace随橘。不熟悉Java異常棧的可參考你真的會閱讀Java的異常信息嗎喂分?。
輪子
調(diào)研讓我們拒絕重復(fù)造輪子
Python3已經(jīng)支持了異常鏈机蔗,通過from關(guān)鍵字即可記錄cause妻顶。
Python2 future包提供的所謂異常鏈raise_from
我是完全沒明白到哪里打印了cause:
from future.utils import raise_from
class DatabaseError(Exception):
pass
class FileDatabase:
def __init__(self, filename):
try:
self.file = open(filename)
except IOError as exc:
raise_from(DatabaseError('failed to open'), exc)
# test
fd = FileDatabase('non_existent_file.txt')
那么,11行拋出的IOError呢蜒车??幔嗦?似乎僅僅多了一句無效信息(future包里的raise e
)酿愧。
Traceback (most recent call last):
File "/Users/mobkeysayhi/PycharmProjects/Wheel/utils/tmp/tmp.py", line 17, in <module>
fd = FileDatabase('non_existent_file.txt')
File "/Users/mobkeysayhi/PycharmProjects/Wheel/utils/tmp/tmp.py", line 13, in __init__
raise_from(DatabaseError('failed to open'), exc)
File "/Library/Python/2.7/site-packages/future/utils/__init__.py", line 454, in raise_from
raise e
__main__.DatabaseError: failed to open
有知道正確姿勢的求點破。
沒找到重復(fù)輪子真是極好的
非常簡單:
import traceback
class TracedError(BaseException):
def __init__(self, msg="", cause=None):
trace_msg = msg
if cause is not None:
_spfile = SimpleFile()
traceback.print_exc(file=_spfile)
_cause_tm = _spfile.read()
trace_msg += "\n" \
+ "\nCaused by:\n\n" \
+ _cause_tm
super(TracedError, self).__init__(trace_msg)
class ErrorWrapper(TracedError):
def __init__(self, cause):
super(ErrorWrapper, self).__init__("Just wrapping cause", cause)
class SimpleFile(object):
def __init__(self, ):
super(SimpleFile, self).__init__()
self.buffer = ""
def write(self, str):
self.buffer += str
def read(self):
return self.buffer
目前只支持單線程模型邀泉,github上有doc和測試用例嬉挡,戳我戳我。
一個測試輸出如下:
Traceback (most recent call last):
File "/Users/monkeysayhi/PycharmProjects/Wheel/utils/exception_chain/traced_error.py", line 68, in <module>
__test()
File "/Users/monkeysayhi/PycharmProjects/Wheel/utils/exception_chain/traced_error.py", line 64, in __test
raise MyError("test MyError", e)
__main__.MyError: test MyError
Caused by:
Traceback (most recent call last):
File "/Users/monkeysayhi/PycharmProjects/Wheel/utils/exception_chain/traced_error.py", line 62, in __test
zero_division()
File "/Users/monkeysayhi/PycharmProjects/Wheel/utils/exception_chain/traced_error.py", line 58, in zero_division
a = 1 / 0
ZeroDivisionError: integer division or modulo by zero
另外汇恤,為方便處理后重新拋出某些異常庞钢,還提供了ErrorWrapper,僅接收一個cause作為參數(shù)因谎。用法如下:
for pid in pids:
# process might have died before getting to this line
# so wrap to avoid OSError: no such process
try:
os.kill(pid, signal.SIGKILL)
except OSError as os_e:
if os.path.isdir("/proc/%d" % int(pid)):
logging.warn("Timeout but fail to kill process, still exist: %d, " % int(pid))
raise ErrorWrapper(os_e)
logging.debug("Timeout but no need to kill process, already no such process: %d" % int(pid))
參考:
本文鏈接:輪子|Python2異常鏈
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布基括,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的财岔,但是必須保留本文的署名及鏈接风皿。