懒惰的记录器消息字符串评估
懒惰的记录器消息字符串评估
我在我的Python应用程序中使用了标准的python logging模块:\n
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("log") while True: logger.debug('Stupid log message " + ' '.join([str(i) for i in range(20)]) ) # Do something
\n问题是,尽管debug级别未启用,但该愚蠢的日志消息在每次循环迭代中都会被评估,这严重影响了性能。\n有没有解决方案?\n在C++中,我们有`log4cxx`包,它提供了类似的宏:\n
LOG4CXX_DEBUG(logger, message)
\n实际上,它会被评估为:\n
if (log4cxx::debugEnabled(logger)) { log4cxx.log(logger,log4cxx::LOG4CXX_DEBUG, message) }
\n但是由于Python中没有宏(据我所知),是否有一种高效的记录日志的方法?
惰性日志消息字符串评估(Lazy logger message string evaluation)问题的出现原因是,当日志级别低于设定级别时,日志消息的参数不会被评估。
在上述代码中,通过使用`logging.basicConfig(level=logging.INFO)`设置日志级别为INFO,并创建了一个名为"log"的logger对象。然后,定义了一个名为Lazy的类,该类的构造函数接收一个函数作为参数,并在其`__str__`方法中调用该函数并返回其结果。接着,通过`logger.debug`和`logger.info`方法分别将Lazy对象作为参数传递给logger对象。
当运行脚本时,可以观察到第一个`logger.debug`命令不需要等待20秒才执行完毕。这是因为在日志级别低于设定级别时,日志消息的参数不会被评估,因此不会执行`time.sleep(20)`这段代码。
为了解决这个问题,可以使用`logger.isEnabledFor`方法来判断日志级别是否高于某个特定级别,然后再决定是否评估日志消息的参数。以下是修改后的代码示例:
import logging import time logging.basicConfig(level=logging.INFO) logger = logging.getLogger("log") class Lazy(object): def __init__(self,func): self.func=func def __str__(self): if logger.isEnabledFor(logging.INFO): # 判断日志级别是否高于INFO级别 return self.func() else: return "" logger.debug(Lazy(lambda: time.sleep(20))) logger.info(Lazy(lambda: "Stupid log message " + ' '.join([str(i) for i in range(20)])))
通过在`__str__`方法中添加判断语句,判断日志级别是否高于INFO级别,如果是,则执行参数评估并返回结果;如果不是,则返回空字符串。这样就可以确保在日志级别低于设定级别时,不会执行耗时的操作,提高了日志记录的效率。
懒惰的日志消息字符串评估(Lazy logger message string evaluation)问题出现的原因是在进行日志记录时,如果直接拼接字符串,会导致每次调用都会进行字符串的拼接操作,而即使日志级别不是DEBUG级别,也会进行不必要的字符串操作。这种方法的效率较低。
解决方法是使用lazyjoin类,将字符串和要拼接的项传入该类的构造函数,然后在类的__str__方法中进行字符串的拼接操作。这样,在日志记录时,只需要将lazyjoin的实例对象传入日志记录函数中,不会进行多余的字符串操作,从而提高了效率。
具体代码如下:
class lazyjoin: def __init__(self, s, items): self.s = s self.items = items def __str__(self): return self.s.join(self.items) logger.debug( 'Stupid log message %s', lazyjoin(' ', (str(i) for i in range(20))) )
根据作者的描述,这种方法比直接拼接字符串的方法要快4倍。作者在描述中还提到了该方法的效率取决于具体的情况,建议根据实际情况进行性能测试。
还有lazyjoin实例对象的创建时间也需要考虑在内。同时,还有自己的实现方法在实际应用中并不需要参数,只需要调用一个方法即可,因此可以在创建lazy对象时只创建一次,然后将对象本身传递进去,而不是每次都创建对象,这样会比使用if判断的方法稍微差一点。作者提供了自己的代码示例供参考。
总结起来,懒惰的日志消息字符串评估问题的出现是由于直接拼接字符串的方法效率较低,解决方法是使用lazyjoin类进行字符串的拼接操作,从而提高效率。同时,根据具体情况考虑lazy对象的创建时间也很重要。
延迟日志消息字符串评估(Lazy logger message string evaluation)是指在日志记录过程中,只有在日志消息实际被记录到某个地方时,才会对日志消息字符串进行完整的评估。这种特性可以通过在日志模块中使用格式化字符串的方式来实现。
在Python的logging模块中,可以使用如下方式来实现延迟日志消息字符串评估:
log.debug("Some message: a=%s b=%s", a, b)
而不是:
log.debug("Some message: a=%s b=%s" % (a, b))
这样,日志模块会智能地避免生成完整的日志消息,除非该消息实际被记录到某个地方。
为了将这种特性应用到特定的需求中,可以创建一个lazyjoin类:
class lazyjoin: def __init__(self, s, items): self.s = s self.items = items def __str__(self): return self.s.join(self.items)
然后可以像下面这样使用它(注意使用了一个生成器表达式来增加延迟性):
logger.info('Stupid log message %s', lazyjoin(' ', (str(i) for i in range(20))))
以上是一个演示,它展示了延迟日志消息字符串评估的工作原理:
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("log") class DoNotStr: def __str__(self): raise AssertionError("the code should not have called this") logger.info('Message %s', DoNotStr()) # 报错,不会调用__str__方法 logger.debug('Message %s', DoNotStr()) # 不会报错,因为没有调用__str__方法
需要注意的是,在Python 3.5及以上版本中,使用f-strings时并不会实现延迟日志消息字符串评估。如果要在f-strings中实现延迟评估,可以参考stackoverflow上的解答(链接:stackoverflow.com/a/49884004/1783801)。