使用 f 字符串样式格式规范记录浮点数特别是“毫秒”时防止舍入或至少长度溢出

问题描述

一点背景:

Python docs 说:

{} 格式的情况下,您可以通过将它们放在属性名称之后来指定格式标志,并用冒号将其分隔。例如:{msecs:03d} 的占位符会将 4 的毫秒值格式化为 004。

然而,这不起作用,因为日志记录的 msecs 值实际上是一个 float

>>> record.msecs
275.38108825683594

因此尝试使用文档中的示例 {} 格式会引发 ValueError:

>>> f'{record.msecs:03d}'
*** ValueError: UnkNown format code 'd' for object of type 'float'

使用 % 的旧式 03d 格式化适用于 float

>>> '%03d' % record.msecs
'275'

基于上述发现,我在日志定义中使用了 float 格式规范,如下所示:

logging.basicConfig(
    level=logging.INFO,format="{asctime}.{msecs:0>3.0f} [{levelname:.1s}] {message}",datefmt="%Y-%m-%d %H:%M:%s",style="{",)

这似乎运行良好,直到我注意到这个日志条目:

2021-03-25 22:37:18.993 [I] ...
2021-03-25 22:37:18.997 [I] ...
2021-03-25 22:37:18.1000 [I] ...
2021-03-25 22:37:19.002 [I] ...
2021-03-25 22:37:19.004 [I] ...

当日志记录的 msecs 浮点值 >= 999.5 时会发生这种情况:

>>> t1 = 999.499999
>>> t2 = 999.5
>>> print(f"{t1:0>3.0f} {t2:0>3.0f}")
999 1000

显然,如果有办法也增加 {{ 1}} 时间戳的一部分。 但是我只记录 .1000(截断为 .000 而不是四舍五入)就可以了,因为这种溢出只发生在很小比例的时间内,而且就我的目的而言此日志记录不是关键。

除了回到基于 {asctime} 的旧式格式之外,还有什么方法可以做到这一点?其他

解决方法

您可以覆盖 default millisecond format

logging.basicConfig(
    level=logging.INFO,format="{asctime} ...{msecs:3.1f} ...{msecs:3.0f} [{levelname:.1s}] {message}",# datefmt="%Y-%m-%d %H:%M:%S",style="{",)
logging.Formatter.default_msec_format = '%s.%03d'

我认为达到你想要的毫秒数被截断为三位数。不幸的是,指定 datefmt 会把它搞砸,毫秒不显示。


以下是自定义 converterformatTime 函数,可用于替换默认的 Formatter 方法。这些将允许您在配置中指定 datefmt。它们协同工作,因此必须同时使用。

import logging

def converter(self,t):
    '''Returns a tuple (time.struct_time,int)
    '''

    # separate seconds from fractional seconds 
    # round the fraction,# adjust the seconds if needed
    # turn the fraction into an integer
    seconds = int(t)
    msec = round(t - seconds,3)
    if msec > .9995:
        msec = 0
        seconds += 1
    else:
        msec = int(1000 * msec)
    # parts = list(time.localtime(seconds))
    # parts[-2] = msec
    t = time.localtime(seconds)
    return t,msec

def formatTime(self,record,datefmt=None):
    """
    Return the creation time of the specified LogRecord as formatted text.
    This method should be called from format() by a formatter which
    wants to make use of a formatted time. This method expects the
    converter to return a (time.struct_time,int) tuple where the int is
    between 0 and 999.
    The int is the rounded msec of the record's creation time.
    """
    ct,msec = self.converter(record.created)
    if datefmt:
        s = time.strftime(datefmt,ct)
    else:
        s = time.strftime(self.default_time_format,ct)
    s = f'{s}.{msec:03d}'
    return s

logging.basicConfig(
    level=logging.INFO,format="{asctime} | {message}",datefmt="%Y-%m-%d %H:%M:%S",)

logging.Formatter.converter = converter
logging.Formatter.formatTime = formatTime
logging.info('foo')

那个日志看起来像:

2021-03-27 16:12:05.498 | foo

如果 datefmt 参数不以秒结束,那么添加毫秒将是愚蠢的,所以也许应该检查一下。

def formatTime(self,ct)
        # s = f'{s}.{msec:03d}'
        # if self.default_msec_format:
        #     s = self.default_msec_format % (s,record.msecs)
    if datefmt[-1] == 'S':
        s = f'{s}.{msec:03d}'
    return s