惯用的 Python 日志记录:格式字符串 + args 列表与内联字符串格式 - 首选哪个?

Posted

技术标签:

【中文标题】惯用的 Python 日志记录:格式字符串 + args 列表与内联字符串格式 - 首选哪个?【英文标题】:Idiomatic Python logging: format string + args list vs. inline string formatting - which is preferred? 【发布时间】:2012-08-10 22:48:42 【问题描述】:

使用格式字符串 + args 列表调用日志记录函数与内联格式相比是否有利?

我见过(并写过)使用内联字符串格式的日志记录代码:

logging.warn("%s %s %s" % (arg1, arg2, arg3))

但我认为使用它更好(在性能方面,并且更惯用):

logging.warn("%s %s %s", arg1, arg2, arg3)

因为第二种形式在调用日志记录函数之前避免了字符串格式化操作。如果当前日志级别会过滤掉日志消息,则无需格式化,从而减少计算时间和内存分配。

我是在正确的轨道上,还是我错过了什么?

【问题讨论】:

我认为你在正确的轨道上。 您有关于在不同时间进行字符串格式化的时间的参考吗?我本来希望两者都发生在拨打warn 之前。不争论你在说什么,只是想了解更多。 嗯,在第一种形式中,我们在调用logging.warn之前进行字符串格式化(元组创建)之前 - 在第二种情况,我们只是将一个 args 列表传递给 logging.warn(这仅仅是创建元组?) - 所以我们避免在调用时进行字符串格式化操作。正如我在问题中提到的,我假设如果当前的日志记录级别会过滤掉日志消息,则不会进行格式化,再次避免字符串格式化操作。我的猜想是这应该节省处理时间和内存分配。 @Inactivist Ah .. 好的,感谢您提供更多信息。我想您可以根据我提供的时间以及您已有的信息得出自己的结论。我想另一种方法可能是计算实际调用的时间并查看它们的比较? Lazy logger message string evaluation 的可能重复项 【参考方案1】:

如果这有帮助,这里是对两个格式选项的快速计时测试:

In [61]: arg1='hello'
In [62]: arg2='this'
In [63]: arg3='is a test'

In [70]: timeit -n 10000000 "%s %s %s" % (arg1, arg2, arg3)
10000000 loops, best of 3: 284 ns per loop

In [71]: timeit -n 10000000  "%s %s %s", arg1, arg2, arg3
10000000 loops, best of 3: 119 ns per loop

似乎给第二种方法带来了优势。

【讨论】:

接下来调用% with log 函数呢? 所以,我可以从你的回答中学到最重要的一课:时间是我的朋友! :D @Inactivist 我发现它很有用.. 经常验证(或反驳)我关于什么是快速的和什么不是的假设:) 是否有类似的简单方法来确定两种变体之间的内存消耗差异? @Inactivist 我也想知道这一点,不幸的是,没有找到了。如果有的话,我很想知道。【参考方案2】:

恕我直言,对于很可能会显示的消息,例如发送给 errorwarn 的消息,这并没有太大的区别。

对于不太可能显示的消息,我肯定会选择第二个版本,主要是出于性能原因。我经常将大对象作为参数提供给info,它实现了一个代价高昂的__str__ 方法。显然,将此预先格式化的内容发送到 info 会浪费性能。

更新

我刚刚检查了logging 模块的源代码,确实,格式化是在检查日志级别之后完成的。例如:

class Logger(Filterer):
    # snip
    def debug(self, msg, *args, **kwargs):
        # snip
        if self.isenabledfor(debug):
            self._log(debug, msg, args, **kwargs)

可以观察到msgargs 在调用log 和检查日志级别之间没有被触及。

更新 2

受 Levon 的启发,让我为具有昂贵的 __str__ 方法的对象添加一些测试:

$ python -m timeit -n 1000000 -s "import logging" -s "logger = logging.getLogger('foo')" -s "logger.setLevel(logging.ERROR)" "logger.warn('%s', range(0,100))"
1000000 loops, best of 3: 1.52 usec per loop
$ python -m timeit -n 1000000 -s "import logging" -s "logger = logging.getLogger('foo')" -s "logger.setLevel(logging.ERROR)" "logger.warn('%s' % range(0,100))"
1000000 loops, best of 3: 10.4 usec per loop

在实践中,这可以带来相当高的性能提升。

【讨论】:

如果日志记录处于性能关键循环中,这可能会有所不同。 这就是我根据观察所期望的。感谢您进行源潜水! Python 3.6 f 字符串格式显示相同的时间损失:python -m timeit -n 1000000 -s "import logging" -s "logger = logging.getLogger('foo')" -s "logger.setLevel(logging.ERROR)" -s "x=list(range(0,100))" "logger.warn(f'x')" 每个循环产生 10 微秒,与经典样式字符串格式相同。将格式化参数直接传递给日志语句会产生 2.12 微秒。【参考方案3】:

如果当前的日志记录级别过滤了日志消息(如我所料),那么避免内联字符串格式确实可以节省一些时间——但不会太多:

In [1]: import logging

In [2]: logger = logging.getLogger('foo')

In [3]: logger.setLevel(logging.ERROR)

In [4]: %timeit -n 1000000 logger.warn('%s %s %s' % ('a', 'b', 'c'))
1000000 loops, best of 3: 1.09 us per loop

In [12]: %timeit -n 1000000 logger.warn('%s %s %s', 'a', 'b', 'c')
1000000 loops, best of 3: 946 ns per loop

因此,作为user1202136 pointed out,整体性能差异取决于格式化字符串所需的时间(这可能很重要,具体取决于在传递给日志记录函数的参数上调用__str__ 的成本。)

【讨论】:

以上是关于惯用的 Python 日志记录:格式字符串 + args 列表与内联字符串格式 - 首选哪个?的主要内容,如果未能解决你的问题,请参考以下文章

Python日志记录使用自定义格式加密

Python3 - Loguru 相见恨晚的输出日志工具

python+Appium自动化:日志logging模块

如何使用中间件以惯用方式记录身份验证信息

Python 中更优雅的日志记录方案

『Python』优雅的记录日志——loguru