目錄 | 上一節 (8.1 測試) | 下一節 (8.3 除錯)
8.2 日誌
本節對日誌模組(logging module)進行簡單的介紹。
logging 模組
logging
模組是用於記錄診斷資訊的 Python 標準庫模組。日誌模組非常龐大,具有許多複雜的功能。我們將會展示一個簡單的例子來說明其用處。
再探異常
在本節練習中,我們建立這樣一個 parse()
函式:
# fileparse.py
def parse(f, types=None, names=None, delimiter=None):
records = []
for line in f:
line = line.strip()
if not line: continue
try:
records.append(split(line,types,names,delimiter))
except ValueError as e:
print("Couldn't parse :", line)
print("Reason :", e)
return records
請看到 try-except
語句,在 except
塊中,我們應該做什麼?
應該列印警告訊息(warning message)?
try:
records.append(split(line,types,names,delimiter))
except ValueError as e:
print("Couldn't parse :", line)
print("Reason :", e)
還是默默忽略警告訊息?
try:
records.append(split(line,types,names,delimiter))
except ValueError as e:
pass
任何一種方式都無法令人滿意,通常情況下,兩種方式我們都需要(使用者可選)。
使用 logging
logging
模組可以解決這個問題:
# fileparse.py
import logging
log = logging.getLogger(__name__)
def parse(f,types=None,names=None,delimiter=None):
...
try:
records.append(split(line,types,names,delimiter))
except ValueError as e:
log.warning("Couldn't parse : %s", line)
log.debug("Reason : %s", e)
修改程式碼以使程式能夠遇到問題的時候發出警告訊息,或者特殊的 Logger
物件。 Logger
物件使用 logging.getLogger(__name__)
建立。
日誌基礎
建立一個記錄器物件(logger object)。
log = logging.getLogger(name) # name is a string
發出日誌訊息:
log.critical(message [, args])
log.error(message [, args])
log.warning(message [, args])
log.info(message [, args])
log.debug(message [, args])
不同方法代表不同級別的嚴重性。
所有的方法都建立格式化的日誌訊息。args
和 %
運算子 一起使用以建立訊息。
logmsg = message % args # Written to the log
日誌配置
配置:
# main.py
...
if __name__ == '__main__':
import logging
logging.basicConfig(
filename = 'app.log', # Log output file
level = logging.INFO, # Output level
)
通常,在程式啟動時,日誌配置是一次性的(譯註:程式啟動後無法重新配置)。該配置與日誌呼叫是分開的。
說明
日誌是可以任意配置的。你可以對日誌配置的任何一方面進行調整:如輸出檔案,級別,訊息格式等等,不必擔心對使用日誌模組的程式碼造成影響。
練習
練習 8.2:將日誌新增到模組中
在 fileparse.py
中,有一些與異常有關的錯誤處理,這些異常是由錯誤輸入引起的。如下所示:
# fileparse.py
import csv
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
'''
Parse a CSV file into a list of records with type conversion.
'''
if select and not has_headers:
raise RuntimeError('select requires column headers')
rows = csv.reader(lines, delimiter=delimiter)
# Read the file headers (if any)
headers = next(rows) if has_headers else []
# If specific columns have been selected, make indices for filtering and set output columns
if select:
indices = [ headers.index(colname) for colname in select ]
headers = select
records = []
for rowno, row in enumerate(rows, 1):
if not row: # Skip rows with no data
continue
# If specific column indices are selected, pick them out
if select:
row = [ row[index] for index in indices]
# Apply type conversion to the row
if types:
try:
row = [func(val) for func, val in zip(types, row)]
except ValueError as e:
if not silence_errors:
print(f"Row {rowno}: Couldn't convert {row}")
print(f"Row {rowno}: Reason {e}")
continue
# Make a dictionary or a tuple
if headers:
record = dict(zip(headers, row))
else:
record = tuple(row)
records.append(record)
return records
請注意發出診斷訊息的 print
語句。使用日誌操作來替換這些 print
語句相對來說更簡單。請像下面這樣修改程式碼:
# fileparse.py
import csv
import logging
log = logging.getLogger(__name__)
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
'''
Parse a CSV file into a list of records with type conversion.
'''
if select and not has_headers:
raise RuntimeError('select requires column headers')
rows = csv.reader(lines, delimiter=delimiter)
# Read the file headers (if any)
headers = next(rows) if has_headers else []
# If specific columns have been selected, make indices for filtering and set output columns
if select:
indices = [ headers.index(colname) for colname in select ]
headers = select
records = []
for rowno, row in enumerate(rows, 1):
if not row: # Skip rows with no data
continue
# If specific column indices are selected, pick them out
if select:
row = [ row[index] for index in indices]
# Apply type conversion to the row
if types:
try:
row = [func(val) for func, val in zip(types, row)]
except ValueError as e:
if not silence_errors:
log.warning("Row %d: Couldn't convert %s", rowno, row)
log.debug("Row %d: Reason %s", rowno, e)
continue
# Make a dictionary or a tuple
if headers:
record = dict(zip(headers, row))
else:
record = tuple(row)
records.append(record)
return records
完成修改後,嘗試在錯誤的資料上使用這些程式碼:
>>> import report
>>> a = report.read_portfolio('Data/missing.csv')
Row 4: Bad row: ['MSFT', '', '51.23']
Row 7: Bad row: ['IBM', '', '70.44']
>>>
如果你什麼都不做,則只會獲得 WARNING
級別以上的日誌訊息。輸出看起來像簡單的列印語句。但是,如果你配置了日誌模組,你將會獲得有關日誌級別,模組等其它資訊。請按以下步驟操作檢視:
>>> import logging
>>> logging.basicConfig()
>>> a = report.read_portfolio('Data/missing.csv')
WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
>>>
你會發現,看不到來自於 log.debug()
操作的輸出。請按以下步驟修改日誌級別(譯註:因為日誌配置是一次性的,所以該操作需要重啟命令列視窗):
>>> logging.getLogger('fileparse').level = logging.DEBUG
>>> a = report.read_portfolio('Data/missing.csv')
WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
DEBUG:fileparse:Row 4: Reason: invalid literal for int() with base 10: ''
WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
DEBUG:fileparse:Row 7: Reason: invalid literal for int() with base 10: ''
>>>
只留下 critical 級別的日誌訊息,關閉其它級別的日誌訊息。
>>> logging.getLogger('fileparse').level=logging.CRITICAL
>>> a = report.read_portfolio('Data/missing.csv')
>>>
練習 8.3:向程式新增日誌
要新增日誌到應用中,你需要某種機制來實現在主模組中初始化日誌。其中一種方式使用看起來像下面這樣的程式碼:
# This file sets up basic configuration of the logging module.
# Change settings here to adjust logging output as needed.
import logging
logging.basicConfig(
filename = 'app.log', # Name of the log file (omit to use stderr)
filemode = 'w', # File mode (use 'a' to append)
level = logging.WARNING, # Logging level (DEBUG, INFO, WARNING, ERROR, or CRITICAL)
)
再次說明,你需要將日誌配置程式碼放到程式啟動步驟中。例如,將其放到 report.py
程式裡的什麼位置?