翻譯:《實用的Python程式設計》08_02_Logging

codists發表於2021-04-10

目錄 | 上一節 (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 除錯)

注:完整翻譯見 https://github.com/codists/practical-python-zh

相關文章