翻譯:《實用的Python編程》08_02_Logging
- 2021 年 4 月 10 日
- 筆記
- Python, 實用的Python編程
目錄 | 上一節 (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
程式里的什麼位置?
目錄 | 上一節 (8.1 測試) | 下一節 (8.3 調試)
註:完整翻譯見 //github.com/codists/practical-python-zh