該文章內容整理自《Python程式設計:從入門到實踐》、《流暢的Python》、以及網上各大部落格
和 C++、Java 這些程式語言一樣,Python 也提供了處理異常的機制 機製,讓 Python 直譯器在程式執行出現錯誤時執行事先準備好的除錯程式,進而嘗試恢復程式的執行。常見異常型別如下
Python 中,用try except語句塊捕獲並處理異常的基本語法結構如下
try:
# 可能產生異常的程式碼塊
except [ (Error1, Error2, ... ) [as e] ]:
# 處理異常的程式碼塊1
except [ (Error3, Error4, ... ) [as e] ]:
# 處理異常的程式碼塊2
except [Exception]:
# 處理其它異常
else:
# 沒有出現異常時進入
finally:
# 最後總會進入
其中
每種異常型別都提供瞭如下幾個屬性和方法,通過呼叫它們,就可以獲取當前處理異常型別的相關資訊:
try except 語句的執行流程如下。首先執行 try 中的程式碼塊,如果執行過程中出現異常,系統會自動生成一個異常型別,並將該異常提交給 Python 直譯器,此過程稱爲捕獲異常。當 Python 直譯器收到異常物件時,會尋找能處理該異常物件的 except 塊,如果找到合適的 except 塊,則把該異常物件交給該 except 塊處理,這個過程被稱爲處理異常。如果 Python 直譯器找不到處理異常的 except 塊,則程式執行終止,Python 直譯器也將退出。事實上,不管程式程式碼塊是否處於 try 塊中,甚至包括 except 塊中的程式碼,只要執行該程式碼塊時出現了異常,系統都會自動生成對應型別的異常。但是,如果此段程式沒有用 try 包裹,又或者沒有爲該異常設定處理它的 except 塊,則 Python 直譯器將無法處理,程式就會停止執行;反之,如果程式發生的異常經 try 捕獲並由 except 處理完成,則程式可以繼續執行
Python 允許使用 raise 語句在程式中手動拋出異常,基本語法爲
raise [exceptionName [(reason)]]
try:
a = input("輸入一個數:")
if(not a.isdigit()):
raise ValueError("a 必須是數位")
except ValueError as e:
print("引發異常:",repr(e))
raise
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)
模組 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,它們的含義分別是:
除了使用 sys.exc_info() 方法獲取更多的異常資訊之外,還可以使用 traceback 模組,該模組可以用來檢視異常的傳播軌跡,追蹤異常觸發的源頭。當異常發生時,會異常從發生異常的函數或方法逐漸向外傳播,首先傳給該函數或方法的呼叫者,該函數或方法的呼叫者再傳給其呼叫者,直至最後傳到 Python 直譯器,此時 Python 直譯器會中止該程式,並列印異常的傳播軌跡資訊
使用 traceback 模組檢視異常傳播軌跡,首先需要將 traceback 模組引入,該模組提供瞭如下兩個常用方法:
在開發過程中,如果出現了問題是很容易使用 Debug 工具來排查的。但程式開發完成,將它部署到生產環境中去之後,這時只能看到其執行的效果而不能直接看到程式碼執行過程中每一步的狀態的。此時,檢查執行情況就會變得非常麻煩。而通過日誌記錄,不論是正常執行還是出現報錯都有相關的時間記錄、狀態記錄、錯誤記錄等,就可以方便地追蹤到在當時的執行過程中出現了的狀況,從而可以快速排查問題
雖然可以將 print 語句輸出重定向到檔案輸出流儲存到檔案中,但這樣做是非常不規範的。在 Python 中有一個標準的 logging 模組來進行標註的日誌記錄,同時還可以做更方便的級別區分以及一些額外日誌資訊的記錄,如時間、執行模組資訊等。總的來說 logging 模組相比 print 有這麼幾個優點:
整個日誌記錄的框架可以分爲這麼幾個部分:
一個簡單例子
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()等方法就可以輸出各個級別的資訊,參數爲需要輸出的內容
下面 下麪詳細介紹basicConfig()函數的參數。這些參數也可以在建立Logger物件後呼叫物件的setLevel()、addHandler()等方法設定
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')
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')
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')