python異常處理的哲學

所謂異常指的是程序的執行出現了非預期行爲,就比如現實中的作一件事過程當中總會出現一些意外的事。異常的處理是跨越編程語言的,和具體的編程細節相比,程序執行異常的處理更像是哲學。限於認知能力和經驗所限,不可能達到像解釋器下import this看到的python設計之禪同樣,本文就結合實際使用簡單的聊一聊。python

0. 前言

工做中,程序員之間一言不合就亮代碼,畢竟不論是代碼自己仍是其執行過程,不會存在二義性,更不會含糊不清,代碼可謂是程序員之間的官方語言。可是其處理問題的邏輯或者算法則並不是如此。程序員

讓我至今記憶猶新的兩次程序員論劍有:算法

反問一:項目後期全部的異常處理都要去掉,不容許上線後出現未知的異常,把你這裏的異常處理去掉,換成if else;編程

反問二:這裏爲何要進行異常處理?代碼都是你寫的,怎麼會出現異常呢?安全

這是我親身經歷的,不知道你們碰到這兩個問題會怎樣回答,至少我當時竟無言以對。這兩個問題分別在不一樣的時間針對不一樣的問題出自一個互聯網巨頭中某個資深QA和資深開發的反問。網絡

暫且不論對錯,畢竟不一樣人考慮問題的出發點是不一樣的。可是從這麼堅定的去異常處理的回答中至少有一點能夠確定,那就是不少人對本身的代碼太過自信或者說是察覺代碼潛在問題的直覺力不夠,更別提正確的處理潛在的問題以保證重要業務邏輯的處理流程。寫代碼的時候若是隻簡單考慮正常的狀況,那是在往代碼中下毒。app

接下類本篇博文將按照套路出牌(避免被Ctrl + W),介紹一下python的異常處理的概念和具體操做.編程語言

1. 爲何要異常處理

常見的程序bug無非就兩大類:ide

  • 語法錯誤;
  • 邏輯不嚴謹或者思惟混亂致使的邏輯錯誤;

顯然第二種錯誤更難被發現,且後果每每更嚴重。不管哪種bug,有兩種後果等着咱們:1、程序崩掉;2、執行結果不符合預期;函數

對於一些重要關鍵的執行操做,異常處理能夠控制程序在可控的範圍執行,固然前提是正確的處理。

好比咱們給第三方提供的API或者使用第三方提供的API。多數狀況下要正確的處理調用者錯誤的調用參數和返回異常結果的狀況,否則就可能要背黑鍋了。

在不可控的環境中運行程序,異常處理是必須的。然而困難的地方是當異常發生時,如何進行處理。

2. python異常處理

下面逐步介紹一下python異常處理相關的概念。

2.1 異常處理結構

必要的結構爲try ... except,至少有一個except,else 和 finally 可選。

try:
    code blocks
except (Exception Class1, Exception Class2, ...) as e:
    catch and process exception
except Exception ClassN:
    catch and process exception
... ...
else:
    when nothing unexpected happened 
finally:
    always executed when all to end

2.2 python 內置異常類型

模塊exceptions中包含了全部內置異常類型,類型的繼承關係以下:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StandardError
      |    +-- BufferError
      |    +-- ArithmeticError
      |    |    +-- FloatingPointError
      |    |    +-- OverflowError
      |    |    +-- ZeroDivisionError
      |    +-- AssertionError
      |    +-- AttributeError
      |    +-- EnvironmentError
      |    |    +-- IOError
      |    |    +-- OSError
      |    |         +-- WindowsError (Windows)
      |    |         +-- VMSError (VMS)
      |    +-- EOFError
      |    +-- ImportError
      |    +-- LookupError
      |    |    +-- IndexError
      |    |    +-- KeyError
      |    +-- MemoryError
      |    +-- NameError
      |    |    +-- UnboundLocalError
      |    +-- ReferenceError
      |    +-- RuntimeError
      |    |    +-- NotImplementedError
      |    +-- SyntaxError
      |    |    +-- IndentationError
      |    |         +-- TabError
      |    +-- SystemError
      |    +-- TypeError
      |    +-- ValueError
      |         +-- UnicodeError
      |              +-- UnicodeDecodeError
      |              +-- UnicodeEncodeError
      |              +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
       +-- ImportWarning
       +-- UnicodeWarning
       +-- BytesWarning
View Code

2.3 except clause

excpet子句的經常使用的寫法以下:

  • except:                         # 默認捕獲全部類型的異常
  • except Exception Class:                   # 捕獲Exception Class類型的異常
  • except Exception Class as e:                 # 捕獲Exception Class類型的異常,異常對象賦值到e
  • except (Exception Class1, Exception Class2, ...) as e:      # 捕獲列表中任意一種異常類型

上面的異常類能夠是下面python內置異常類型,也能夠是自定義的異常類型。

2.4 異常匹配原則

  • 全部except子句按順序一一匹配,匹配成功則忽略後續的except子句;
  • 若拋出異常對象爲except子句中給出的異常類型的對象或給出的異常類型的派生類對象,則匹配成功;
  • 若是全部的except子句均匹配失敗,異常會向上傳遞;
  • 若是依然沒有被任何try...except捕獲到,程序在終止前會調用sys.excepthook進行處理;

2.5 else & finally

若是沒有異常發生,且存在else子句,則執行else子句。只要存在finally子句,不管任何狀況下都會被執行。

可能惟一很差理解的地方就是finally。沒有異常、捕獲異常、異常上傳以及異常處理過程當中發生異常等均會執行finally語句。

下面看個例子:

def division(a, b):
    try:
        print'res = %s' % (a / b)
    except (ZeroDivisionError, ArithmeticError) as e:
        return str(e)  # 注意此處使用的是return else:
        print '%s / %s = %s' % (a, b, a / b)
    finally:
        print 'finally clause'

 分別輸入參數(1, 2),(1, 0)和 (1,「0」)執行:

print 'return value: %s' % division(a, b)

獲得的結果以下:

res = 0
1 / 2 = 0
finally clause
return value: None

finally clause
return value: integer division or modulo by zero

finally clause
Traceback (most recent call last):
  File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 217, in <module>
    print 'return value: %s' % division(1, "0")
  File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 208, in division
    print'res = %s' % (a / b)
TypeError: unsupported operand type(s) for /: 'int' and 'str'
View Code

能夠看到縱使程序發生異常且沒有被正確處理,在程序終止前,finally語句依舊被執行了。能夠將此看作程序安全的最後一道有效屏障。主要進行一些善後清理工做,好比資源釋放、斷開網絡鏈接等。固然with聲明能夠自動幫咱們進行一些清理工做。

2.6 raise拋出異常

程序執行過程當中可使用raise主動的拋出異常. 

try:
    e = Exception('Hello', 'World')
    e.message = 'Ni Hao!'
    raise e
except Exception as inst:
    print type(inst), inst, inst.args, inst.message

結果:<type 'exceptions.Exception'> ('Hello', 'World') ('Hello', 'World') Ni Hao!

上面展現了except對象的屬性args, message。

2.7 自定義異常

絕大部分狀況下內置類型的異常已經可以知足平時的開發使用,若是想要自定義異常類型,能夠直接繼承內置類型來實現。

class ZeroDivZeroError(ZeroDivisionError):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self)
    def __repr__(self):
        return self.value

try:
    # do something and find 0 / 0
    raise ZeroDivZeroError('hahajun')
except ZeroDivZeroError as err:
    print 'except info %s' % err

 自定義異常應該直接繼承自Exception類或其子類,而不要繼承自BaseException.

3. Stack Trace

python執行過程當中發生異常,會告訴咱們到底哪裏出現問題和什麼問題。這兩種類型的錯誤信息分別爲stack trace和 exception,在程序中分別用traceback object和異常對象表示。

Traceback (most recent call last):
  File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 270, in <module>
    1 / 0
ZeroDivisionError: integer division or modulo by zero

上面的錯誤信息包含錯誤發生時當前的堆棧信息(stack trace, 前三行)和異常信息(exception,最後一行),分別存放在traceback objects和拋出的異常對象中。

異常對象及異常信息前面已經介紹過,接下來咱們在看一下異常發生時,stack trace的處理。

Traceback objects represent a stack trace of an exception. A traceback object is created when an exception occurs.

這時有兩種狀況:

  1. 異常被try...except捕獲
  2. 沒有被捕獲或者乾脆沒有處理

正常的代碼執行過程,可使用traceback.print_stack()輸出當前調用過程的堆棧信息。

3.1 捕獲異常

 對於第一種狀況可使用下面兩種方式獲取stack trace信息:

trace_str = traceback.format_exc()
或者從sys.exc_info()中獲取捕獲的異常對象等的信息,而後格式化成trace信息。
def get_trace_str(self):
    """
    從當前棧幀或者以前的棧幀中獲取被except捕獲的異常信息;
    沒有被try except捕獲的異常會直接傳遞給sys.excepthook
    """
    t, v, tb = sys.exc_info()
    trace_info_list = traceback.format_exception(t, v, tb)
    trace_str = ' '.join(trace_info_list)

至於拋出的包含異常信息的異常對象則能夠在try...except結構中的except Exception class as e中獲取。 

3.2 未捕獲異常

第二種狀況,若是異常沒有被處理或者未被捕獲則會在程序推出前調用sys.excepthook將traceback和異常信息輸出到sys.stderr。

def except_hook_func(tp, val, tb):
    trace_info_list = traceback.format_exception(tp, val, tb)
    trace_str = ' '.join(trace_info_list)
    print 'sys.excepthook'
    print trace_str
sys.excepthook = except_hook_func

上面自定義except hook函數來取代sys.excepthook函數。在hook函數中根據異常類型tp、異常值和traceback對象tb獲取stack trace。這種狀況下不能從sys.exc_info中獲取異常信息。

3.3 測試

def except_hook_func(tp, val, tb):
    trace_info_list = traceback.format_exception(tp, val, tb)
    trace_str = ' '.join(trace_info_list)
    print 'sys.excepthook'
    print trace_str
sys.excepthook = except_hook_func
try:
    1 / 0
except TypeError as e:
    res = traceback.format_exc()
    print "try...except"
    print str(e.message)
    print res

走的是sys.excepthook處理流程結果:

sys.excepthook
Traceback (most recent call last):
   File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 259, in <module>
    1 / 0
 ZeroDivisionError: integer division or modulo by zero

將except TypeError as e 改成 except ZeroDivisionError as e,則走的是try...except捕獲異常流程,結果以下:

try...except
integer division or modulo by zero
Traceback (most recent call last):
  File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 259, in <module>
    1 / 0
ZeroDivisionError: integer division or modulo by zero

4. 異常信息收集

講了這麼多,咱們看一下如何實現一個程序中trace信息的收集。

 1 class TracebackMgr(object):
 2 
 3     def _get_format_trace_str(self, t, v, tb):
 4         _trace = traceback.format_exception(t, v, tb)
 5         return ' '.join(_trace)
 6 
 7     def handle_one_exception(self):
 8         """
 9         從當前棧幀或者以前的棧幀中獲取被except捕獲的異常信息;
10         沒有被try except捕獲的異常會自動使用handle_traceback進行收集
11         """
12         t, v, tb = sys.exc_info()
13         self.handle_traceback(t, v, tb, False)
14 
15     def handle_traceback(self, t, v, tb, is_hook = True):
16         """
17         將此函數替換sys.excepthook以可以自動收集沒有被try...except捕獲的異常,
18         使用try except處理的異常須要手動調用上面的函數handle_one_exception纔可以收集
19         """
20         trace_str = self._get_format_trace_str(t, v, tb)
21         self.record_trace(trace_str, is_hook)
22         # do something else
23 
24     def record_trace(self, trace_str, is_hook):
25         # Do somethind
26         print 'is_hook: %s' % is_hook
27         print trace_str

其用法很簡單:

trace_mgr = TracebackMgr()
sys.excepthook = trace_mgr.handle_traceback
try:
    1 / 0
except Exception as e:
    trace_mgr.handle_one_exception()
    # process trace

1 / '0'

結果用兩種方式收集到兩個trace信息:

is_hook: False
Traceback (most recent call last):
   File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 299, in <module>
    1 / 0
 ZeroDivisionError: integer division or modulo by zero

is_hook: True
Traceback (most recent call last):
   File "D:\My Folders\Cnblogs\Alpha Panda\Main.py", line 304, in <module>
    1 / '0'
 TypeError: unsupported operand type(s) for /: 'int' and 'str'

 能夠將標準的輸入和輸出重定向,將打印日誌和錯誤信息輸入到文件中:

class Dumpfile(object):
    @staticmethod
    def write(str_info):
        with open('./dump_file.txt', 'a+') as fobj:
            fobj.write(str_info)

    def flush(self):
        self.write('')
sys.stdout = sys.stderr = Dumpfile()

trace的收集主要用到兩點:如何捕獲異常和兩種狀況下異常信息的收集,前面都介紹過。

5. 總結

python 異常處理:

  1. 使用對象來表示異常錯誤信息,每種異常均有一種對應的類,BaseException爲全部表示異常處理類的基類。
  2. 程序執行過程當中拋出的異常會匹配該對象對應的異常類和其全部的基類。
  3. 能夠從內置類型的異常類派生出自定義的異常類。
  4. 被捕獲的異常能夠再次被拋出。
  5. 能夠的話儘可能使用內置的替代方案,如if getattr(obj, attr_name, None),或者with結構等。
  6. sys.exc_info()保存當前棧幀或者以前的棧幀中獲取被try, except捕獲的異常信息。
  7. 未處理的異常致使程序終止前會被sys.excpethook處理,能夠自定義定義sys.excpethook。

異常的陷阱:

正確的異常處理能讓代碼有更好的魯棒性,可是錯誤的使用異常會過猶不及。

捕獲異常卻忽略掉或者錯誤的處理是不可取的。濫用異常處理不只達不到提升系統穩定性的效果,還會隱藏掉引發錯誤的誘因,致使排查問題的難度增長。

所以好比何捕獲異常更重要的是,異常發生時應當如何處理。

相關文章
相關標籤/搜索