【Python】第八章 異常

2020-08-09 09:17:52

該文章內容整理自《Python程式設計:從入門到實踐》、《流暢的Python》、以及網上各大部落格

異常

和 C++、Java 這些程式語言一樣,Python 也提供了處理異常的機制 機製,讓 Python 直譯器在程式執行出現錯誤時執行事先準備好的除錯程式,進而嘗試恢復程式的執行。常見異常型別如下

  1. AssertionError:當 assert 關鍵字後的條件爲假時,程式執行會停止並拋出 AssertionError 異常。如assert(False)
  2. AttributeError:試圖存取的物件屬性不存在
  3. FileNotFoundError:輸入輸出異常,無法開啓檔案
  4. ImportError:無法引入模組或包,路徑檔案錯誤
  5. IndentationError:語法錯誤,程式碼沒有對齊
  6. IndexError:索引超出序列範圍
  7. KeyboardInterrupt:按下Ctrl+C中斷程式
  8. KeyError:字典中查詢一個不存在的關鍵字
  9. NameError:嘗試存取一個未宣告的變數
  10. SyntaxError:程式碼非法,無法編譯
  11. TypeError:不同類型數據之間的無效操作
  12. ZeroDivisionError:除法運算中除數爲 0

try

Python 中,用try except語句塊捕獲並處理異常的基本語法結構如下

try:
    # 可能產生異常的程式碼塊
except [ (Error1, Error2, ... ) [as e] ]:
    # 處理異常的程式碼塊1
except [ (Error3, Error4, ... ) [as e] ]:
    # 處理異常的程式碼塊2
except  [Exception]:
    # 處理其它異常
else: 
    # 沒有出現異常時進入
finally:
    # 最後總會進入

其中

  • try 塊有且僅有一個,但 except 程式碼塊可以有多個,並且每個 except 塊都可以同時處理多種異常,如(Error1, Error2,…) 、(Error3, Error4,…),但不推薦這種寫法。
  • [as e]作爲可選參數,表示給這些異常型別起一個別名 e,這樣做的好處是方便在 except 塊中呼叫異常型別。
  • [Exception]作爲可選參數,可以代指程式可能發生的所有異常情況,其通常用在最後一個 except 塊。一般來說,可處理全部異常的 except 塊 Exception 要放到所有 except 塊的後面,父類別異常的 except 塊要放到子類異常的 except 塊的後面。
  • 在try except後也可以加上else程式碼塊,當try塊中出現錯誤時會被except塊捕獲,而當沒有出現錯誤時則會進入else塊。else塊是在出現異常時才起作用,當沒有出現異常時會進入到else塊,然後try except塊外的程式碼;而當出現異常時,則不執行else塊程式碼,在處理完異常後直接執行try except外的程式碼
  • 最後還可以加上finally塊。finally塊只與try匹配,而與except或else塊無關。無論 try 塊是否發生異常,最終都要進入 finally 語句,並執行其中的程式碼塊。如當 try 塊打開了一些物理資源時,由於這些資源必須手動回收,則回收工作通常就放在 finally 塊中。因爲一旦 try 塊中的某行程式碼發生異常,則其後續的程式碼將不會得到執行;其次 except 和 else 也不適合,它們都可能不會得到執行。而 finally 塊中的程式碼,無論 try 塊是否發生異常,該塊中的程式碼都會被執行

每種異常型別都提供瞭如下幾個屬性和方法,通過呼叫它們,就可以獲取當前處理異常型別的相關資訊:

  • args:返回異常的錯誤編號和描述字串
  • str(e):返回異常資訊,但不包括異常資訊的型別
  • repr(e):返回較全的異常資訊,包括異常資訊的型別

try except 語句的執行流程如下。首先執行 try 中的程式碼塊,如果執行過程中出現異常,系統會自動生成一個異常型別,並將該異常提交給 Python 直譯器,此過程稱爲捕獲異常。當 Python 直譯器收到異常物件時,會尋找能處理該異常物件的 except 塊,如果找到合適的 except 塊,則把該異常物件交給該 except 塊處理,這個過程被稱爲處理異常。如果 Python 直譯器找不到處理異常的 except 塊,則程式執行終止,Python 直譯器也將退出。事實上,不管程式程式碼塊是否處於 try 塊中,甚至包括 except 塊中的程式碼,只要執行該程式碼塊時出現了異常,系統都會自動生成對應型別的異常。但是,如果此段程式沒有用 try 包裹,又或者沒有爲該異常設定處理它的 except 塊,則 Python 直譯器將無法處理,程式就會停止執行;反之,如果程式發生的異常經 try 捕獲並由 except 處理完成,則程式可以繼續執行

raise

Python 允許使用 raise 語句在程式中手動拋出異常,基本語法爲
raise [exceptionName [(reason)]]

try:
    a = input("輸入一個數:")
    if(not a.isdigit()):
        raise ValueError("a 必須是數位")
except ValueError as e:
    print("引發異常:",repr(e))
    raise
  1. raise:單獨一個 raise。該語句引發當前上下文中捕獲的異常(比如在 except 塊中),如上面程式中已經手動引發了 ValueError 異常,因此這裏當再使用 raise 語句時,它會再次引發一次;或預設引發 RuntimeError 異常。
  2. raise exceptionName:raise 後帶一個異常類名稱,表示引發執行型別的異常
  3. raise exceptionName(reason):在引發指定型別的異常的同時,附帶異常的描述資訊

assert

Python 提供了 assert 語句用來偵錯程式。assert 語句的完整語法格式爲
assert 條件表達式 [,描述資訊]
當條件表達式的值爲真時,該語句什麼也不做,程式正常執行;反之,若條件表達式的值爲假,則 assert 會拋出 AssertionError 異常。其中,[,描述資訊] 作爲可選參數,用於對條件表達式可能產生的異常進行描述

try:
    s_age = input("請輸入您的年齡:")
    age = int(s_age)
    assert 20 < age < 80 , "年齡不在 20-80 之間"
    print("您輸入的年齡在20和80之間")
except AssertionError as e:
    print("輸入年齡不正確", e)

另外,當在命令列模式執行 Python 程式時傳入 -O(大寫)參數,可以禁用程式中包含的 assert 語句

自定義異常

Python 提供了大量的異常類,這些異常類之間有嚴格的繼承關係
在这里插入图片描述

可見 BaseException 是 Python 中所有異常類的基礎類別,但一般來說程式中可能出現的各種異常都繼承自 Exception,因而 Exception 是萬能錯誤攔截,可以攔下所有錯誤。同時,自定義異常也應該繼承 Exception 類而非 BaseException 類

下面 下麪是自定義異常類的簡單例子

class MyException(Exception):
	def __init__(self, msg):
		self.message = msg
	def __str__(self):
		return self.message

try:
	raise MyException("New Exception")
except MyException as e:
	print(e)

獲取異常資訊

exc_info()

模組 sys 中,有兩個方法可以返回異常的全部資訊,分別是 exc_info() 和 last_traceback(),這兩個函數有相同的功能和用法。這裏只介紹exc_info()函數

import sys
import traceback
try:
    # ...
except:
    print(sys.exc_info())
    traceback.print_tb(sys.exc_info()[2])

exc_info() 方法會將當前的異常資訊以元組的形式返回,該元組中包含 3 個元素,分別爲 type、value 和 traceback,它們的含義分別是:

  • type:異常型別的名稱,它是 BaseException 的子類。如<class ‘ZeroDivisionError’>
  • value:捕獲到的異常範例。如ZeroDivisionError(‘division by zero’,)
  • traceback:是一個 traceback 物件。要檢視 traceback 物件包含的內容,需要先引進 traceback 模組,然後呼叫 traceback 模組中的 print_tb 方法,並將 sys.exc_info() 輸出的 traceback 物件作爲參數參入。輸出資訊中包含了更多的異常資訊,包括檔名、拋出異常的程式碼所在的行數、拋出異常的具體程式碼

traceback 模組

除了使用 sys.exc_info() 方法獲取更多的異常資訊之外,還可以使用 traceback 模組,該模組可以用來檢視異常的傳播軌跡,追蹤異常觸發的源頭。當異常發生時,會異常從發生異常的函數或方法逐漸向外傳播,首先傳給該函數或方法的呼叫者,該函數或方法的呼叫者再傳給其呼叫者,直至最後傳到 Python 直譯器,此時 Python 直譯器會中止該程式,並列印異常的傳播軌跡資訊

使用 traceback 模組檢視異常傳播軌跡,首先需要將 traceback 模組引入,該模組提供瞭如下兩個常用方法:

  • print_exc([limit[, file]]):將異常傳播軌跡資訊輸出到控制檯或指定檔案中。其中limit用於限制顯示異常傳播的層數。比如函數 A 呼叫函數 B,函數 B 發生了異常,如果指定 limit=1,則只顯示函數 A 裏面發生的異常。如果不設定 limit 參數,則預設全部顯示。file指定將異常傳播軌跡資訊輸出到指定檔案中。如果不指定該參數,則預設輸出到控制檯
  • format_exc([limit[, chain]]):將異常傳播軌跡資訊轉換成字串。limit限制顯示異常傳播的層數。chain預設爲True,也就是一併顯示__cause__、__context__等串連起來的異常

logging

在開發過程中,如果出現了問題是很容易使用 Debug 工具來排查的。但程式開發完成,將它部署到生產環境中去之後,這時只能看到其執行的效果而不能直接看到程式碼執行過程中每一步的狀態的。此時,檢查執行情況就會變得非常麻煩。而通過日誌記錄,不論是正常執行還是出現報錯都有相關的時間記錄、狀態記錄、錯誤記錄等,就可以方便地追蹤到在當時的執行過程中出現了的狀況,從而可以快速排查問題

雖然可以將 print 語句輸出重定向到檔案輸出流儲存到檔案中,但這樣做是非常不規範的。在 Python 中有一個標準的 logging 模組來進行標註的日誌記錄,同時還可以做更方便的級別區分以及一些額外日誌資訊的記錄,如時間、執行模組資訊等。總的來說 logging 模組相比 print 有這麼幾個優點:

  • 可以在 logging 模組中設定日誌等級,在不同的版本(如開發環境、生產環境)上通過設定不同的輸出等級來記錄對應的日誌,非常靈活
  • print 的輸出資訊都會輸出到標準輸出流中,而 logging 模組就更加靈活,可以設定輸出到任意位置,如寫入檔案、寫入遠端伺服器等
  • logging 模組具有靈活的設定和格式化功能,如設定輸出當前模組資訊、執行時間等,相比 print 的字串格式化更加方便易用

整個日誌記錄的框架可以分爲這麼幾個部分:

  • Logger:Logger Main Class,是日誌記錄時建立的物件。可以呼叫它的方法傳入日誌模板和資訊,來生成一條條日誌記錄,稱作 Log Record
  • Log Record:生成的一條條日誌記錄
  • Handler:用來處理日誌記錄的類,可以將 Log Record 輸出到指定的日誌位置和儲存形式等。如可以指定將日誌通過 FTP 協定記錄到遠端的伺服器上
  • Formatter:實際上生成的 Log Record 也是一個個物件,如果想要將其儲存成一條條日誌文字,則需要有一個格式化的過程,而這個過程就由 Formatter 來完成,返回日誌字串,然後傳回給 Handler 來處理
  • Filter:儲存日誌時可能不需要全部儲存,如只儲存某個級別的日誌,或只儲存包含某個關鍵字的日誌等,所以儲存前還需要進行過濾,而這個過濾過程就交給 Filter 來完成
  • Parent Handler:Handler 之間可以存在分層關係,以使得不同 Handler 之間共用相同功能的程式碼

一個簡單例子

import logging
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
 
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

其中,basicConfig()函數用來進行日誌的全域性設定。getLogger()方法用來宣告一個Logger物件,初始化時需要傳入了模組的名稱,這裏直接使用 __name__ ,即模組的名稱來代替,若不傳入則爲 __main__,若爲 import 的模組的話就是被引入模組的名稱,這個變數在不同的模組中的名字是不同的,所以一般使用 __name__ 來表示。呼叫物件裡的info()、debug()、warning()等方法就可以輸出各個級別的資訊,參數爲需要輸出的內容

Logging設定

下面 下麪詳細介紹basicConfig()函數的參數。這些參數也可以在建立Logger物件後呼叫物件的setLevel()、addHandler()等方法設定

  • filename:日誌輸出的檔名,如果指定了這個資訊之後,實際上會啓用 FileHandler,而不再是 StreamHandler,這樣日誌資訊便會輸出到檔案中了
  • filemode:這個是指定日誌檔案的寫入方式,有兩種形式,一種是 w,一種是 a,分別代表清除後寫入和追加寫入
  • format:指定日誌資訊的輸出格式,即上文範例所示的參數,部分常用參數如下所示:
    • %(levelno)s:列印日誌級別的數值
    • %(levelname)s:列印日誌級別的名稱
    • %(pathname)s:列印當前執行程式的路徑,其實就是sys.argv[0]
    • %(filename)s:列印當前執行程式名
    • %(funcName)s:列印日誌的當前函數
    • %(lineno)d:列印日誌的當前行號
    • %(asctime)s:列印日誌的時間
    • %(thread)d:列印執行緒ID
    • %(threadName)s:列印執行緒名稱
    • %(process)d:列印進程ID
    • %(processName)s:列印執行緒名稱
    • %(module)s:列印模組名稱
    • %(message)s:列印日誌資訊
import logging
 
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')
  • datefmt:指定時間的輸出格式,如 ‘%Y/%m/%d %H:%M:%S’
  • style:如果 format 參數指定了,這個參數就可以指定格式化時的佔位符風格,如 %、{、$ 等
  • level:指定日誌輸出的類別,程式會輸出大於等於此級別的資訊。個等級及數值如下:CRITICAL:50、FATAL:50、ERROR:40、WARNING:30、WARN:30、INFO:20、DEBUG:10、NOTSET:0
  • stream:在沒有指定 filename 的時候會預設使用 StreamHandler,這時 stream 可以指定初始化的檔案流
  • handlers:可以指定日誌處理時所使用的 Handlers,必須是可迭代的。logging 模組提供了多種 Handler,每個 Handler 還可以各自的設定資訊
    • StreamHandler:logging.StreamHandler;日誌輸出到流,可以是 sys.stderr,sys.stdout 或者檔案
    • FileHandler:logging.FileHandler;日誌輸出到檔案
    • BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日誌回滾方式
    • RotatingHandler:logging.handlers.RotatingHandler;日誌回滾方式,支援日誌檔案最大數量和日誌檔案回滾
    • TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日誌回滾方式,在一定時間區域內回滾日誌檔案
    • SocketHandler:logging.handlers.SocketHandler;遠端輸出日誌到TCP/IP sockets
    • DatagramHandler:logging.handlers.DatagramHandler;遠端輸出日誌到UDP sockets
    • SMTPHandler:logging.handlers.SMTPHandler;遠端輸出日誌到郵件地址
    • SysLogHandler:logging.handlers.SysLogHandler;日誌輸出到syslog
    • NTEventLogHandler:logging.handlers.NTEventLogHandler;遠端輸出日誌到Windows NT/2000/XP的事件日誌
    • MemoryHandler:logging.handlers.MemoryHandler;日誌輸出到記憶體中的指定buffer
    • HTTPHandler:logging.handlers.HTTPHandler;通過」GET」或者」POST」遠端輸出到HTTP伺服器
import logging
from logging.handlers import HTTPHandler
import sys
 
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
 
# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)
 
# FileHandler
file_handler = logging.FileHandler('output.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
 
# HTTPHandler
http_handler = HTTPHandler(host='localhost:8001', url='log', method='POST')
logger.addHandler(http_handler)
 
# Log
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

捕獲Traceback

Logging也可以捕獲Traceback,在 error() 方法中將 exc_info 設定爲 True,這樣就可以輸出執行過程中的資訊了

import logging
 
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler = logging.FileHandler('result.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

logger.info('Start')
logger.warning('Something maybe fail.')
try:
    result = 10 / 0
except Exception:
    logger.error('Faild to get result', exc_info=True)
logger.info('Finished')

設定共用

在寫專案的時候,如果每個檔案都來設定 logging 設定那就太繁瑣了,logging 模組提供了父子模組共用設定的機制 機製,會根據 Logger 的名稱來自動載入父模組的設定
如在main.py檔案將 Logger 的名稱定義爲 main

import logging
import core
 
logger = logging.getLogger('main')
logger.setLevel(level=logging.DEBUG)
 
# Handler
handler = logging.FileHandler('result.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
 
logger.info('Main Info')
logger.debug('Main Debug')
logger.error('Main Error')
core.run()

則在core.py檔案中可將 Logger 的名稱定義爲 main.core,這樣 core.py 裏面的 Logger 就會複用 main.py 裏面的 Logger 設定,而不用再去設定一次了

import logging
 
logger = logging.getLogger('main.core')
 
def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')

如此一來,只要在入口檔案裏面定義好 logging 模組的輸出設定,子模組只需要在定義 Logger 物件時名稱使用父模組的名稱開頭即可共用設定,非常方便

檔案設定

在開發過程中,將設定在程式碼裏面寫死並不是一個好的習慣,更好的做法是將設定寫在組態檔裏面,然後執行時讀取組態檔裏面的設定,這樣更方便管理和維護。如定義一個 yaml 組態檔,其中 root 指定了 handlers 是 console,即只輸出到控制檯。另外在 loggers 一項設定裏面,我們定義了 main.core 模組,handlers 是 console、file、error 三項,即輸出到控制檯、輸出到普通檔案和回滾檔案

version: 1
formatters:
  brief:
    format: "%(asctime)s - %(message)s"
  simple:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
  console:
    class : logging.StreamHandler
    formatter: brief
    level   : INFO
    stream  : ext://sys.stdout
  file:
    class : logging.FileHandler
    formatter: simple
    level: DEBUG
    filename: debug.log
  error:
    class: logging.handlers.RotatingFileHandler
    level: ERROR
    formatter: simple
    filename: error.log
    maxBytes: 10485760
    backupCount: 20
    encoding: utf8
loggers:
  main.core:
    level: DEBUG
    handlers: [console, file, error]
root:
  level: DEBUG
  handlers: [console]

再在main.py檔案中呼叫

import logging
import core
import yaml
import logging.config
import os
 
 
def setup_logging(default_path='config.yaml', default_level=logging.INFO):
    path = default_path
    if os.path.exists(path):
        with open(path, 'r', encoding='utf-8') as f:
            config = yaml.load(f)
            logging.config.dictConfig(config)
    else:
        logging.basicConfig(level=default_level)
 
 
def log():
    logging.debug('Start')
    logging.info('Exec')
    logging.info('Finished')
 
 
if __name__ == '__main__':
    yaml_path = 'config.yaml'
    setup_logging(yaml_path)
    log()

core.py檔案

import logging
 
logger = logging.getLogger('main.core')
 
def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')