译自:https://www.machinelearningplus.com/python/python-logging-guide
作者:Patrick Smith
Python 中的 logging 模块可以跟踪代码运行时的事件,当程序崩溃时可以查看日志并且发现是什么引发了错误。Log 信息有内置的层级——调试(debugging)、信息(informational)、警告(warnings)、错误(error)和严重错误(critical)。也可以在 logging 中包含 traceback 信息。不管是小项目还是大项目,都推荐在 Python 程序中使用 logging。本文将简单清晰地介绍如何使用 logging 模块。
为什么使用 logging?
当你运行一个 Python 脚本时,你可能想要知道脚本的哪个部分在执行,并且检视变量的当前值。
通常,可以只使用 print()
打印出你想要的信息。在小程序中,可能靠这个就足够了。
但问题是,当你处理有很多个模块的大项目时,就需要一个更加灵活的方法。
为什么?
因为代码需要经历开发、调试、审查、测试或者上线等不同阶段。在开发时你想要打印的信息类型可能和上线后你想看到的信息类型完全不同。
也就是说,在“测试”时,你可能只想看警告和错误信息,然而在“调试”时,你可能还想看到跟调试相关的信息。
如果你还想打印出使用的模块以及代码运行的时间,那么你的代码很容易变得混乱。
使用 logging
模块,这些问题就能很容易地解决。
logging
模块可以:
- 控制信息层级,仅记录需要的信息。
- 控制显示或者保存日志的时机。
- 使用内置信息模板控制日志格式。
- 知晓信息来自于哪个模块。
基本 logging 例子
logging
模块是 Python 的标准库,要使用 logging,只需要使用 logging.basicConfig()
进行基本设置。事实上,这也是可选的。
然后就可以调用 logging.{level}(message)
在控制台中显示信息。
import logging
logging.basicConfig(level=logging.INFO)
def hypotenuse(a, b):
"""计算三角形斜边"""
return (a**2 + b**2)**0.5
logging.info("{a}, {b} 的斜边是 {c}".format(a=3, b=4, c=hypotenuse(a=3, b=4)))
#> INFO:root:3, 4 的斜边是 5.0
打印出的日志信息遵循默认格式: {LEVEL}:{LOGGER}:{MESSAGE}
。
上面的例子中, LEVEL
就是 INFO
,因为调用的是 logging.info()
。
LOGGER
为 root
,因为这是默认 logger。
logger(日志记录器)类似于一个实体,你可以创建并配置它来记录不同类型和格式的消息。
你可以配置一个输出到控制台的 logger 和另一个将日志发送到文件的 logger,它们具有不同的日志记录级别,并且特定于给定模块。
最后,输出的信息就是我传递给 logging.info()
的字符串。
那么如果不设置 logging.basicConfig(level=logging.INFO)
会怎么样?
答案是 日志信息不会被打印出来。
为什么?要知道这个需要先了解 logging 的级别。
logging 的 5 个级别
logging
有 5 个不同层次的日志级别,可以将给定的 logger 配置为这些级别:
- DEBUG:详细信息,用于诊断问题。Value=10。
- INFO:确认代码运行正常。Value=20。
- WARNING:意想不到的事情发生了,或预示着某个问题。但软件仍按预期运行。Value=30。
- ERROR:出现更严重的问题,软件无法执行某些功能。Value=40。
- CRITICAL:严重错误,程序本身可能无法继续运行。Value=50。
现在,让我们回答之前提出的问题。默认 logger 是 root
,其默认的 basicConfig 级别是 WARNING
。也就是说,只有来自 logging.warning
或者更高级别的信息才会被记录下来。
因此,logging.info()
中的信息不会被打印出来。这也是为什么 basicConfig 被设为 INFO
。
如果级别使用 logging.ERROR
代替,只有来自 logging.error
和 logging.critical
的信息会被记录。
import logging
logging.basicConfig(level=logging.ERROR)
def hypotenuse(a, b):
"""计算三角形斜边"""
return (a**2 + b**2)**0.5
kwargs = {'a':3, 'b':4, 'c':hypotenuse(3, 4)}
logging.debug("a = {a}, b = {b}".format(**kwargs))
logging.info("{a}, {b} 的斜边是 {c}".format(**kwargs))
logging.warning("a={a} 和 b={b} 相等".format(**kwargs))
logging.error("a={a} 和 b={b} 不能为负".format(**kwargs))
logging.critical("{a}, {b} 的斜边是 {c}".format(**kwargs))
#> ERROR:root:a=3 和 b=4 不能为负
#> CRITICAL:root:3, 4 的斜边是 5.0
将日志记入文件
要从 root logger 将日志消息发送到文件,需要在 logging.basicConfig()
中设置 file 参数:
import logging
logging.basicConfig(level=logging.INFO, filename='sample.log')
现在,所有后续日志消息都将直接记录到当前工作目录中的“sample.log“文件。如果要将其记录到另一个目录中的文件,请给出完整的文件路径。
如何更改 logging 格式
logging 模块提供了向日志消息添加各种详细信息的速记表。
让我们更改日志信息格式以显示 TIME
、LEVEL
和 MESSAGE
。
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s :: %(levelname)s :: %(message)s')
logging.info("当当当!")
#> 2019-03-10 19:41:09,057 :: INFO :: 当当当!
不要对所有模块使用 root logger
让我们看下面的代码:
# 1. myprojectmodule.py
import logging
logging.basicConfig(filename='module.log')
#-----------------------------
# 2. main.py (从 myprojectmodule.py 导入代码)
import logging
import myprojectmodule # 运行 myprojectmodule.py 中的代码,将生成 `module.log` 文件
logging.basicConfig(filename='main.log') # 无效!
如果项目中有一个或多个模块。这些模块使用基本根模块。然后,当导入模块 myprojectmodule.py
时,将运行该模块的所有代码并配置 logger。
一旦配置好,main
文件中的 root logger 将不能再更改 root logger 设置。因为,一旦设置好 logging.basicConfig()
,就不能再更改它。
如果想在不同文件中使用不同 logger,就需要创建一个新的 logger。
如何创建一个新的 logger?
可以使用 logger.getLogger(name)
方法创建一个新的 logger。如果存在同名的 logger,则将使用该 logger。
可以给 logger 取任何名字,但是通常使用 __name__
变量:
logger = logging.getLogger(__name__)
logger.info('my logging message')
__name__
变量保存调用代码的模块名称。因此,当在模块中使用 logging 时,将会创建一个 logger,其值由模块 __name__
属性提供。
这样做的好处是,如果将来更改模块名,就不必修改内部代码。
另一个需要注意的地方是,所有的 logger 都有一个内置的层次结构。
例如,如果你已配置 root logger 将消息记录到特定文件。还拥有一个自定义的 logger,但还没有为其配置 file handler 来将消息发送到控制台或其他日志文件。
在这种情况下,自定义 logger 将回退并写入 root logger 本身设置的文件,直到配置了自定义 logger 的日志文件。
那么什么是 file handler 以及如何设置它呢?
File Handler 和 Formatter
FileHandler()
和 Formatter()
类用于为 root logger 之外的 logger 设置输出文件和消息格式。
当创建一个单独的 logger 时,需要使用 logging.FileHandler()
和 logging.Formatter()
对象分别进行设置。
FileHandler
用于将自定义 logger 记录到另一个文件。同样,Formatter
用于更改已记录消息的格式。
import logging
# 获取或创建一个 logger
logger = logging.getLogger(__name__)
# 设置日志级别
logger.setLevel(logging.WARNING)
# 定义 FileHandler 并设置 Formatter
file_handler = logging.FileHandler('logfile.log')
formatter = logging.Formatter('%(asctime)s : %(levelname)s : %(name)s : %(message)s')
file_handler.setFormatter(formatter)
# 添加 FileHandler 至 logger
logger.addHandler(file_handler)
# 日志
logger.debug('调试')
logger.info('信息')
logger.warning('警告')
logger.error('错误')
logger.critical('严重错误')
假设上面的代码是从主程序运行的,如果查看工作目录,将创建一个名为 logfile.log
的文件,并包含以下消息:
2019-03-10 20:17:40,552 : WARNING : __main__ : 警告
2019-03-10 20:17:40,552 : ERROR : __main__ : 错误
2019-03-10 20:17:40,552 : CRITICAL : __main__ : 严重错误
再次注意,Formatter
是在 FileHandler
对象上设置的,而不是直接在 logger 上设置。
如何在日志消息中包含 traceback 信息
使用 logger.exception
,可以在代码遇到任何错误时记录 traceback 信息。logger.exception
将记录其参数中提供的消息以及 traceback 信息。
import logging
# 获取或创建 logger
logger = logging.getLogger(__name__)
# 设置日志级别
logger.setLevel(logging.INFO)
def divide(x, y):
try:
out = x / y
except ZeroDivisionError:
logger.exception("除零问题")
else:
return out
# 日志
logger.error("Divide {x} / {y} = {c}".format(x=10, y=0, c=divide(10, 0)))
#> 除零问题
#> Traceback (most recent call last):
#> File "<ipython-input-16-a010a44fdc0a>", line 12, in divide
#> out = x / y
#> ZeroDivisionError: division by zero
#> Divide 10 / 0 = None