目录 | 上一节 (8.1 测试) | 下一节 (8.3 调试)

8.2 日志

本节对日志模块(logging module)进行简单的介绍。

logging 模块

logging 模块是用于记录诊断信息的 Python 标准库模块。日志模块非常庞大,具有许多复杂的功能。我们将会展示一个简单的例子来说明其用处。

再探异常

在本节练习中,我们创建这样一个 parse() 函数:

  1. # fileparse.py
  2. def parse(f, types=None, names=None, delimiter=None):
  3. records = []
  4. for line in f:
  5. line = line.strip()
  6. if not line: continue
  7. try:
  8. records.append(split(line,types,names,delimiter))
  9. except ValueError as e:
  10. print("Couldn't parse :", line)
  11. print("Reason :", e)
  12. return records

请看到 try-except 语句,在 except 块中,我们应该做什么?

应该打印警告消息(warning message)?

  1. try:
  2. records.append(split(line,types,names,delimiter))
  3. except ValueError as e:
  4. print("Couldn't parse :", line)
  5. print("Reason :", e)

还是默默忽略警告消息?

  1. try:
  2. records.append(split(line,types,names,delimiter))
  3. except ValueError as e:
  4. pass

任何一种方式都无法令人满意,通常情况下,两种方式我们都需要(用户可选)。

使用 logging

logging 模块可以解决这个问题:

  1. # fileparse.py
  2. import logging
  3. log = logging.getLogger(__name__)
  4. def parse(f,types=None,names=None,delimiter=None):
  5. ...
  6. try:
  7. records.append(split(line,types,names,delimiter))
  8. except ValueError as e:
  9. log.warning("Couldn't parse : %s", line)
  10. log.debug("Reason : %s", e)

修改代码以使程序能够遇到问题的时候发出警告消息,或者特殊的 Logger 对象。 Logger 对象使用 logging.getLogger(__name__) 创建。

日志基础

创建一个记录器对象(logger object)。

  1. log = logging.getLogger(name) # name is a string

发出日志消息:

  1. log.critical(message [, args])
  2. log.error(message [, args])
  3. log.warning(message [, args])
  4. log.info(message [, args])
  5. log.debug(message [, args])

不同方法代表不同级别的严重性。

所有的方法都创建格式化的日志消息。args% 运算符 一起使用以创建消息。

  1. logmsg = message % args # Written to the log

日志配置

配置:

  1. # main.py
  2. ...
  3. if __name__ == '__main__':
  4. import logging
  5. logging.basicConfig(
  6. filename = 'app.log', # Log output file
  7. level = logging.INFO, # Output level
  8. )

通常,在程序启动时,日志配置是一次性的(译注:程序启动后无法重新配置)。该配置与日志调用是分开的。

说明

日志是可以任意配置的。你可以对日志配置的任何一方面进行调整:如输出文件,级别,消息格式等等,不必担心对使用日志模块的代码造成影响。

练习

练习 8.2:将日志添加到模块中

fileparse.py 中,有一些与异常有关的错误处理,这些异常是由错误输入引起的。如下所示:

  1. # fileparse.py
  2. import csv
  3. def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
  4. '''
  5. Parse a CSV file into a list of records with type conversion.
  6. '''
  7. if select and not has_headers:
  8. raise RuntimeError('select requires column headers')
  9. rows = csv.reader(lines, delimiter=delimiter)
  10. # Read the file headers (if any)
  11. headers = next(rows) if has_headers else []
  12. # If specific columns have been selected, make indices for filtering and set output columns
  13. if select:
  14. indices = [ headers.index(colname) for colname in select ]
  15. headers = select
  16. records = []
  17. for rowno, row in enumerate(rows, 1):
  18. if not row: # Skip rows with no data
  19. continue
  20. # If specific column indices are selected, pick them out
  21. if select:
  22. row = [ row[index] for index in indices]
  23. # Apply type conversion to the row
  24. if types:
  25. try:
  26. row = [func(val) for func, val in zip(types, row)]
  27. except ValueError as e:
  28. if not silence_errors:
  29. print(f"Row {rowno}: Couldn't convert {row}")
  30. print(f"Row {rowno}: Reason {e}")
  31. continue
  32. # Make a dictionary or a tuple
  33. if headers:
  34. record = dict(zip(headers, row))
  35. else:
  36. record = tuple(row)
  37. records.append(record)
  38. return records

请注意发出诊断消息的 print 语句。使用日志操作来替换这些 print 语句相对来说更简单。请像下面这样修改代码:

  1. # fileparse.py
  2. import csv
  3. import logging
  4. log = logging.getLogger(__name__)
  5. def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
  6. '''
  7. Parse a CSV file into a list of records with type conversion.
  8. '''
  9. if select and not has_headers:
  10. raise RuntimeError('select requires column headers')
  11. rows = csv.reader(lines, delimiter=delimiter)
  12. # Read the file headers (if any)
  13. headers = next(rows) if has_headers else []
  14. # If specific columns have been selected, make indices for filtering and set output columns
  15. if select:
  16. indices = [ headers.index(colname) for colname in select ]
  17. headers = select
  18. records = []
  19. for rowno, row in enumerate(rows, 1):
  20. if not row: # Skip rows with no data
  21. continue
  22. # If specific column indices are selected, pick them out
  23. if select:
  24. row = [ row[index] for index in indices]
  25. # Apply type conversion to the row
  26. if types:
  27. try:
  28. row = [func(val) for func, val in zip(types, row)]
  29. except ValueError as e:
  30. if not silence_errors:
  31. log.warning("Row %d: Couldn't convert %s", rowno, row)
  32. log.debug("Row %d: Reason %s", rowno, e)
  33. continue
  34. # Make a dictionary or a tuple
  35. if headers:
  36. record = dict(zip(headers, row))
  37. else:
  38. record = tuple(row)
  39. records.append(record)
  40. return records

完成修改后,尝试在错误的数据上使用这些代码:

  1. >>> import report
  2. >>> a = report.read_portfolio('Data/missing.csv')
  3. Row 4: Bad row: ['MSFT', '', '51.23']
  4. Row 7: Bad row: ['IBM', '', '70.44']
  5. >>>

如果你什么都不做,则只会获得 WARNING 级别以上的日志消息。输出看起来像简单的打印语句。但是,如果你配置了日志模块,你将会获得有关日志级别,模块等其它信息。请按以下步骤操作查看:

  1. >>> import logging
  2. >>> logging.basicConfig()
  3. >>> a = report.read_portfolio('Data/missing.csv')
  4. WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
  5. WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
  6. >>>

你会发现,看不到来自于 log.debug() 操作的输出。请按以下步骤修改日志级别(译注:因为日志配置是一次性的,所以该操作需要重启命令行窗口):

  1. >>> logging.getLogger('fileparse').level = logging.DEBUG
  2. >>> a = report.read_portfolio('Data/missing.csv')
  3. WARNING:fileparse:Row 4: Bad row: ['MSFT', '', '51.23']
  4. DEBUG:fileparse:Row 4: Reason: invalid literal for int() with base 10: ''
  5. WARNING:fileparse:Row 7: Bad row: ['IBM', '', '70.44']
  6. DEBUG:fileparse:Row 7: Reason: invalid literal for int() with base 10: ''
  7. >>>

只留下 critical 级别的日志消息,关闭其它级别的日志消息。

  1. >>> logging.getLogger('fileparse').level=logging.CRITICAL
  2. >>> a = report.read_portfolio('Data/missing.csv')
  3. >>>

练习 8.3:向程序添加日志

要添加日志到应用中,你需要某种机制来实现在主模块中初始化日志。其中一种方式使用看起来像下面这样的代码:

  1. # This file sets up basic configuration of the logging module.
  2. # Change settings here to adjust logging output as needed.
  3. import logging
  4. logging.basicConfig(
  5. filename = 'app.log', # Name of the log file (omit to use stderr)
  6. filemode = 'w', # File mode (use 'a' to append)
  7. level = logging.WARNING, # Logging level (DEBUG, INFO, WARNING, ERROR, or CRITICAL)
  8. )

再次说明,你需要将日志配置代码放到程序启动步骤中。例如,将其放到 report.py 程序里的什么位置?

目录 | 上一节 (8.1 测试) | 下一节 (8.3 调试)