Python常用模块功能简介(三)logging

logging基本介绍

先介绍一下我们为什么要使用日志,平常我们编写程序为了验证程序运行与debug,通常会使用print函数来对一些中间结果进行输出验证,在验证成功后再将print语句注释或删除掉。这样做在小型程序中还比较灵活,但是对于大型项目来说,就十分繁琐了----->所以使用日志log就很自然了,日志可以调整日志级别,来决定我们是否输出对应级别的日志,同时还可以将日志导入文件记录下来。

再介绍一下logging中的log级别

Level Numeric Value
logging.CRITICAL 50
logging.ERROR 40
logging.WARNING 30
logging.INFO 20
logging.DEBUG 10

实际上这些level都是整数值,可由如type(logging.info)验证为int类型。

模块级的使用方法

实际上logging是一个package而不是module,以下对于源码的讨论都是在logging的__init__.py文件中的
logging模块的模块级使用方法就是使用一些模块级接口函数。而且还有一个比较重要的是对日志的输出形式和输出目的地等进行设置。

接口函数也是对应日志级别而输出信息的
logging.debug(msg)
logging.info(msg)
logging.warning(msg)
logging.error(msg)
logging.critical(msg)
这几个函数除了日志级别上的区别,其实都是使用默认的root logger来对信息进行log的,它是处于日志器层级关系最顶层的日志器,且该实例是以单例模式存在的。见源码:

def info(msg, *args, **kwargs):
    if len(root.handlers) == 0:
        basicConfig()
    root.info(msg, *args, **kwargs)

这里可以见到在logging模块的info函数中:(1)首先进行了一个对于root logger的handlers属性的长度判断是否调用basicConfig函数。(2)之后是调用root logger的info函数来实现功能的。
这里对于第(2)点我们进一下探寻:

root = RootLogger(WARNING)

在logging的源码中可以看到如上语句,即我们将logging模块import后,其实已经默认的创建了一个root logger对象,并且logging自己维护有一个Logger实例的hierarchy(通过Manager实例对象,它和root logger一样也是单例的使用模式),我们自己创建的logger实例对象,都是是root logger的孩子

日志的设置

对于日志的设置,我们是使用logging.basicConfig(**kwargs)函数,它是对root logger进行设置的,一般使用较多的关键字参数如下:

  • level 决定root logger的日志级别。
  • format 它是日志格式字符串,指定日志输出的字段信息,如日期,日志级别,文件名,当然还有我们的msg信息等等。
  • datefmt 决定日期字段的输出格式。
  • filename 日志信息的输出目的地文件,不指定时默认输出到控制台。
  • filemode 目的地文件的打开模式,不指定默认为"a"。

对于logging.basicConfig函数有一点需要注意:我们不能在该函数前使用任何模块级日志输出函数如logging.info、logging.error,因为它们会调用一个不带参的basicConfig函数,使得logging.basicConfig函数失效。见源码(由于代码过多,建议参考注释进行阅读):

def basicConfig(**kwargs):
    _acquireLock()
    try:
        #这里由于不带参调用basicConifg,
        #而root.handlers默认为空列表
        #在Logger定义中可见self.handlers被设为[],
        #而默认的root实例在创建时只指定了log级别
        #所以if条件必然通过
        if len(root.handlers) == 0: 
            #由于不带参,所以handlers必为None
            handlers = kwargs.pop("handlers", None)
            if handlers is None:
                #这里由于不带参,所以即是handlers为None
                #通过上面的if判断,但kwargs同样为None,
                #所以该if不通过
                if "stream" in kwargs and "filename" in kwargs:
                    raise ValueError("'stream' and 'filename' should not be "
                                     "specified together")
            else:
                if "stream" in kwargs or "filename" in kwargs:
                    raise ValueError("'stream' or 'filename' should not be "
                                     "specified together with 'handlers'")
            #这里由于handlers为None通过if判断继续执行
            if handlers is None:
                filename = kwargs.pop("filename", None)
                mode = kwargs.pop("filemode", 'a')
                if filename:
                    h = FileHandler(filename, mode)
                #不带参,kwargs为None,所以filename
                #在上面的执行语句的返回值为None,所以
                #执行这个else分支
                else:
                    stream = kwargs.pop("stream", None)
                    h = StreamHandler(stream)
                #注意这里,十分重要,可见handlers终于不为None
                #被赋予了一个列表,该列表有一个元素h
                handlers = [h]
            dfs = kwargs.pop("datefmt", None)
            style = kwargs.pop("style", '%')
            if style not in _STYLES:
                raise ValueError('Style must be one of: %s' % ','.join(
                                 _STYLES.keys()))
            fs = kwargs.pop("format", _STYLES[style][1])
            fmt = Formatter(fs, dfs, style)
            #再看这里,十分重要
            for h in handlers:
                #这个无所谓,就是对format进行默认设置
                if h.formatter is None:
                    h.setFormatter(fmt)
                #这里最为关键,可见root.addHandler(h)函数
                #会把h添加进root.handlers列表中,那么很显然
                #root.handlers不再是一个空列表
                root.addHandler(h)
            level = kwargs.pop("level", None)
            if level is not None:
                root.setLevel(level)
            if kwargs:
                keys = ', '.join(kwargs.keys())
                raise ValueError('Unrecognised argument(s): %s' % keys)
    finally:
        _releaseLock()

所以即是不带参调用basicConfig(),但是经过其函数体执行,root.handlers的列表长度会不为0,所以之后再调用logging.basicConifg函数时,对root.handlers判断,就会因此而直接略过函数体中try部分(主要部分),直接执行finally,没有进行任何设置。(并且很关键的basicConfig函数就是对root logger的handlers进行设置

对象级使用

在logging模块中logger对象从来都不是直接实例化,而是通过一个模块级借口完成:logging.getLogger(name=None),注意我们创建的logger都是root logger的子类。而通过我们自己创建的logger对象,使用日志记录也是和模块级接口一样的:
logger.debug(msg)
logger.info(msg)
logger.warning(msg)
logger.error(msg)
logger.critical(msg)
同样对于日志格式的设置也是通过logging.basicConfig函数完成的,虽然该函数是对root logger实例对象的日志格式设置,但由于Logger类方法的特殊实现,其实是能够沿用该设置的。
并且对于对象级接口,如logger.info函数:

    def info(self, msg, *args, **kwargs):
        if self.isEnabledFor(INFO):
            self._log(INFO, msg, args, **kwargs)

可见其中没有对basicConfig函数的调用,所以也就没有修改root.handlers列表,即不会发生上文的logging.basciConfig函数失效的问题

Logger类及basicConfig沿用问题(无兴趣可略)

上文提到了由于Logger类中方法的特殊实现,使得之后实例化的logger对象在进行日志输出记录时也能沿用root logger的basicConfig设定。而这背后的机制到底是怎样的呢?先看:

class RootLogger(Logger):
    def __init__(self, level):
        """
        Initialize the logger with the name "root".
        """
        Logger.__init__(self, "root", level)

_loggerClass = Logger
.....
#由上面代码可见RootLogger其实就是调用了Logger类
root = RootLogger(WARNING)
#这里是对Logger类绑定两个类级属性
#把root实例作为Logger类的一个root属性
Logger.root = root
#把root实例作为参数传入Manager类,
#并用返回的实例定义Logger类的manager属性
#并且Manager类只会被实例化这一次
Logger.manager = Manager(Logger.root)

以上源码中出现的Manager类很重要,在我们的logging.getLogger函数中:

def getLogger(name=None):
    """
    Return a logger with the specified name, 
    creating it if necessary.
    If no name is specified, return the root logger.
    """
    if name:
        #结合上文的代码,我们使用root实例作为参数
        #传入Manager类定义了Logger.manager属性,
        #此处使用manager实例所有的getLogger方法
        #来具体实现模块级的getLogger方法
        return Logger.manager.getLogger(name)
    else:
        return root

小结:
Logger.manager属性是Manager(root)的实例,而logging.getLogger其实就是由Logger.manager对象调用它的getLogger方法。
再转到Manager类的定义中,来分析Manager(Logger.root)创建manager实例对象和用logger.getLogger时具体发生了什么:

class Manager(object):
    """
    holds the hierarchy of loggers.
    """
    #Logger.manager = Manager(Logger.root)发生的事情
    def __init__(self, rootnode):
        """
        Initialize the manager with the root node of the logger hierarchy.
        """
        #结合上文Logger.manager = Manager(Logger.root)
        #可见我们使用root logger作为rootnode,并把它赋予
        #Manager类的self.root属性
        self.root = rootnode
        self.disable = 0
        self.emittedNoHandlerWarning = False
        #self.loggerDict维护logger hierarchy中的loggers
        self.loggerDict = {}
        self.loggerClass = None
        self.logRecordFactory = None
        
    #使用logging.getLogger时发生的事情,Logger.manager属性
    #也就是使用root logger作为参数的Manager实例调用该实例
    #所属Manager类的getLogger方法
    def getLogger(self, name):
        #这里创建一个值为None的rv
        rv = None
        #不用理会,只是判断name值是否为str
        if not isinstance(name, str):
            raise TypeError('A logger name must be a string')
        _acquireLock()
        try:
            #这里由于是使用Logger.manager实例来调用
            #getLogger方法,而Logger.manager实例的
            #self.loggerDict初始值为空,所以在第一
            #次使用getLogger方法时,这个判断不通过
            #就算之后再次调用logging.getLogger(name)
            #也要看name是否在loggerDict中
            if name in self.loggerDict:
                rv = self.loggerDict[name]
                if isinstance(rv, PlaceHolder):
                    ph = rv
                    rv = (self.loggerClass or _loggerClass)(name)
                    rv.manager = self
                    self.loggerDict[name] = rv
                    self._fixupChildren(ph, rv)
                    self._fixupParents(rv)
            #所以直接转到此处
            else:
                #这里self.loggerClass初始为空,使用
                #_loggerClass其实就是Logger类实例化
                #一个logger对象附于rv
                rv = (self.loggerClass or _loggerClass)(name)
                #这里再把self也就是Logger.manager赋予
                #rv.manager,并未创建新的Manager实例
                rv.manager = self
                #把rv加入loggerDict中,name:rv键值对
                self.loggerDict[name] = rv
                #设定当前logger实例的parent
                self._fixupParents(rv)
        finally:
            _releaseLock()
        #返回rv,也就是创建的logger实例对象
        return rv

大概介绍了Manager的部分源码,我们回到最初的问题,为何我们自己创建的logger实例能沿用root logger的basicConfig设定,再看logger.info方法的源码:

    
    #logger实例的info方法
    def info(self, msg, *args, **kwargs):
        #对比INFO日志级别和当前logger实例的日志级别来决定是否进行log
        if self.isEnabledFor(INFO):
            #所以这里其实是调用了logger实例的底层_log方法
            self._log(INFO, msg, args, **kwargs)
            
            
    def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
        """
        Low-level logging routine which 
        creates a LogRecord and then calls
        all the handlers of this logger to
        handle the record.
        """
        #为避免繁琐,把一些简单使用时不会碰到的代码略过
        ...
        #生成log record
        record = self.makeRecord(self.name, level, fn, lno, msg, args,
                                 exc_info, func, extra, sinfo)
        #调用当前logger实例的handle方法来处理log record
        self.handle(record)
        
        
    def handle(self, record):
        if (not self.disabled) and self.filter(record):
            #可见logger类的handle方法又调用了该类的
            #callHandlers方法来处理record
            self.callHandlers(record)
            
    #注意看源码中自带注释对于callHandlers方法解释,其中
    #很重要的一点就是,该函数会loop through该logger乃至
    #该logger的parent,parent的parent一直到root logger
    #中的handlers,注意我们在前文中说了,basicConfig其实
    #就是对root logger的handlers进行设置
    def callHandlers(self, record):
        """
        Pass a record to all relevant handlers.
        Loop through all handlers for this logger 
        and its parents in the logger hierarchy. 
        If no handler was found, output a one-off error
        message to sys.stderr. Stop searching up 
        the hierarchy whenever a logger with the 
        "propagate" attribute set to zero is found - that
        will be the last logger whose handlers are called.
        """
        #把当前logger实例赋予c
        c = self
        found = 0
        #这个while一定可以进去,因为c是一个logger对象,不为None
        while c:
            #对于我们创建的logger,它的handlers初始值是空列表
            #所以这个for一开始进不去,对于我们的简单使用场景
            #使用了logging.basicConfig(...),然后创建自己的
            #logger=logging.getLogger("mylogger"),当前
            #logger的parent肯定是root logger,而前面的
            #basicConfig对root logger的handlers设置了,
            #所以root logger的handlers不为空,可以进入
            for hdlr in c.handlers:
                found = found + 1
                if record.levelno >= hdlr.level:
                    #使用handlers中的handler对
                    #log record进行handle处理
                    hdlr.handle(record)
            #这个也进不去,logger实例的propagate都是默认True
            if not c.propagate:
                c = None    #break out
            #只能进这里了
            else:
                #把当前logger实例的parent赋予c
                #然后继续循环,如果handlers仍然为空
                #则继续把parent赋予c直到root logger
                c = c.parent
       ...
    

简而言之,我们自己创建的logger能使用parent(Manager实例所维持的一个hierarchy)的handlers对log信息进行handle,而向上追溯的祖先就是root logger,我们已经通过basicConfig的话,实质上就是对它的handlers进行了设置,所以在这个hierarchy中的后代便能享用。

你可能感兴趣的:(python,日志)