引言
logging 的基本用法网上很多,这里就不介绍了。在引入正文之前,先来看一个需求:
假设需要将某功能封装成类库供他人使用,如何处理类库中的日志?
数年前在一个 C# 开发的项目中,我用了这样的方法:定义一个 logging 基类,所有需要用到日志的类都继承这个基类,这个基类中定义一个 LogHandler 事件,该事件用于实现具体的记录日志动作,同时可以通过将类 A 的 LogHandler 委托挂到类 B 的 LogHandler 上,实现将两个类的日志信息添加到一起。
自从看了 python 中 logging 的实现方式,我发现我的做法真是弱爆了。
我在之前的博客 Python:logging.NullHandler 的使用 中介绍了 peewee
框架中的日志输出,简单来说就是 peewee
中定义了一个名为peewee
的Logger
并添加了一个 NullHandler
,调用者只需要为其添加具体的Handler
就可以输出日志了,非常方便。
假设我们在主程序中也有一个 Logger
,调用 peewee
后,我想将两个日志输出到同一个日志文件中去。显然将两个日志的 FileHandler
指向同一个日志文件是不可取的,存在并发抢占文件的风险。当然我们也可以将主程序中的 Logger
名字定为 peewee
,但这不仅太 low 了,而且如果再调用一个库,其中也封装好了一个 Logger
,就不好处理了。
树桩结构的 Logger
Logger
对象被设计为一个树形结构,它有一个 parent
属性。logging
中定义了一个名为 root
的 Logger
作为所有 Logger
的根节点,root
的 parent
属性为 None
。root
是全局的。
当调用
logging.getLogger(name=None)
得到一个 Logger
对象的时候,如果 name
为 None
,则返回根节点root
。如果 name
中含有 .
,比如 name = 'a.b'
,这时如果已经存在了名为 a
的 Logger
,则 a.b
为 a
的子节点,如果不存在名为 a
的Logger
,则 a.b
为 root
的子节点。
child logger 在完成对日志消息的处理后,默认会将日志消息传递给与它的parent logger。因此,我们不必为一个应用程序中使用的所有 Logger
定义和配置 handlers,只需要为一个顶层的 Logger
配置 handlers,然后按照需要创建 child loggers 就可足够了。我们可以通过设置 Logger
的propagate
属性设置为 False
来关闭这种传递机制。
什么意思呢,我们来看代码:
import logging
logA = logging.getLogger('a')
logA.setLevel(logging.DEBUG)
logA.addHandler(logging.StreamHandler())
logB = logging.getLogger('a.b')
logB.addHandler(logging.StreamHandler())
输出结果:
Logger A
Logger B
Logger B
之所以 Logger B
被输出了 2 次,是因为 logB
是 logA
的子节点,并且logB
中也定义了 Handler,所以 logB
的 Handler 输出了一次,logA
的Handler 也输出了一次,就 2 次了。如果想只输出一次,可以删掉 logB
中的 Handler
。当然,这也是有用处的,尤其是当你手头没有日志管理工具的时候。例如,主程序中需要输出所有的日志,以便了解程序整体的运行顺序,而某模块的日志,你想单独输出一份,以便清晰了解模块中的报错或者是执行顺序。
之前 peewee
的例子也就很容易解决了,只需要将 peewee
日志的 parent
属性设置为主程序的日志就可以了。
结语
其实这是一个比较容易说明的问题,完全没必要写这么多。我并不想跟大家分享 python 中的 logging 是怎么用的,而是想和大家分享 logging 如此实现的一种思想,因为我遇到过这个问题,也设计了解决方案,然后被完爆了。