輪子|Python2異常鏈

介紹一個本身造的輪子,Python2異常鏈。html

需求

習慣了用java擼碼,雖然說膠水代碼多,但能比較好的用代碼表達思路;而Python則簡潔到了簡陋的地步——各類雞肋的語法糖,各類不完善的機制。好比錯誤處理。java

Python2沒有異常鏈,讓問題排查變得很是困難python

# 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...")
複製代碼

打印異常以下:git

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
複製代碼

不考慮代碼風格,是標準的Python2異常處理方式:分別捕獲異常,再統一成一個異常,只有msg不一樣,從新拋出。這種寫法又醜又冗餘,頂多能夠改爲這樣:github

try:
    print divide(a, b)
except StandardError as e:
    # m_counter.inc("err", 1)
    raise UnexpectedError(e.message)
複製代碼

即使如此,也沒法解決一個最嚴重的問題:明明是11行拋出的異常,但打印出來的異常棧卻只能追蹤到22行從新拋出異常的raise語句。重點在於沒有記錄cause,使咱們追蹤到22行以後,不知道爲何會拋出cause,也就沒法定位到實際發生問題的代碼。bash

異常鏈

最理想的方式,仍是在異常棧中打印異常鏈:app

try:
    print divide(a, b)
except StandardError as cause:
    # m_counter.inc("err", 1)
    raise UnexpectedError("some msg", cause)
複製代碼

就像Java的異常棧,區分「要拋出的異常UnexpectedError和引發該異常的緣由cause」:ide

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致使;1行與9行下是各異常的調用路徑trace。不熟悉Java異常棧的可參考你真的會閱讀Java的異常信息嗎?測試

輪子

調研讓咱們拒絕重複造輪子

Python3已經支持了異常鏈,經過from關鍵字便可記錄cause。ui

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
複製代碼

有知道正確姿式的求點破。

沒找到重複輪子真是極好的

很是簡單:

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做爲參數。用法以下:

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
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索