将日志记录的“print”函数更改为“tqdm.write”,以便日志记录不会干扰进度条。

35

我有一个简单的问题:如何将内置的Python日志记录器的print函数更改为tqdm.write,以便日志记录消息不会干扰tqdm的进度条?


4
如果你必须使用tqdm,那么@RolKau的答案是可行的,但是每次写入时tqdm会清除并重新绘制所有进度条,所以如果输出量很大,很容易压垮它。如果你不一定要用tqdm,Enlighten可以直接满足你的需求,并且在负载下会更加稳定,因为它不依赖于重新绘制。 - aviso
6个回答

59

tqdm现在具有内置的上下文管理器,用于重定向记录器:

import logging
from tqdm import trange
from tqdm.contrib.logging import logging_redirect_tqdm

LOG = logging.getLogger(__name__)

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    with logging_redirect_tqdm():
        for i in trange(9):
            if i == 4:
                LOG.info("console logging redirected to `tqdm.write()`")
    # logging restored

从tqdm文档复制


3
以上所有答案中,只有这一个有效。应该标记为被采纳的答案。 - Ani
2
如果您已经为 LOG 设置了自己的格式化程序,则应该执行 logging_redirect_tqdm(loggers=[LOG])。这将导致 tqdm 使用 LOG 的格式化程序。 - Daniel Walker
这个答案比被采纳的答案更为更新,更有效,并且更加直观。 - Martim Passos
不过,无论在哪里使用tqdm.write,都需要一个上下文管理器,而另一种解决方案可以用于全局更改。 - Guillochon

47
您需要一个自定义的日志处理程序:
import logging
import tqdm

class TqdmLoggingHandler(logging.Handler):
    def __init__(self, level=logging.NOTSET):
        super().__init__(level)

    def emit(self, record):
        try:
            msg = self.format(record)
            tqdm.tqdm.write(msg)
            self.flush()
        except Exception:
            self.handleError(record)  

然后将这个添加到日志链中:

import time

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
log.addHandler(TqdmLoggingHandler())
for i in tqdm.tqdm(range(100)):
    if i == 50:
        log.info("Half-way there!")
    time.sleep(0.1)

编辑:在评论中勤奋的读者@BlaineRogers指出了一个调用super TqdmLoggingHandler的init方法的错误,已经进行了修复。(如果有人想进一步了解Python这个模糊领域,请参考https://fuhm.net/super-harmful/)


我需要使用记录模块进行输出,像这样解决了它:https://dev59.com/oGUp5IYBdhLWcg3w-Laz#41224909。输出结果如下所示:`2016-12-19 15:35:06 [INFO ] 16%|#####9 | 768/4928 [07:04<40:50, 1.70it/s]` - ddofborg
1
重要的是使用import tqdm而不是from tqdm import tqdm,否则IO会中断进度条。 - Alexander McFarlane
如果您尝试实例化TqdmLoggingHandler的子类,则super(self.__class__, self).__init__(level)会导致RecursionErrorsuper(cls, self).__init__()调用self.__class__方法解析顺序中cls之后的类的__init__方法。假设MyTqdmLoggingHandlerTqdmLoggingHandler的子类,则MyTqdmLoggingHandler()调用TqdmLoggingHandler.__init__(self),该方法调用super(MyTqdmLoggingHandler, self).__init__(),进而调用TqdmLoggingHandler.__init__(self) - Blaine Rogers
1
@BlaineRogers 您说得完全正确!感谢您指出这一点。显然在我熟悉MRO之前,我写下了这个答案。如果有人想使用代码,我会更正答案。 - RolKau
我认为提到古老的“超级有害”对任何人都没有好处。请观看雷蒙德·赫廷格的演讲“超级考虑!”以获得彻底的反驳。https://youtu.be/EiOglTERPEo - traal
由于我们处于Jupyter Notebook中,应该使用tqdm.notebook.tqdm.write而不是tqdm.tqdm.write - robertspierre

4

基于RolKau上面的回答,简化版:

import logging
from tqdm import tqdm


class TqdmLoggingHandler(logging.StreamHandler):
    """Avoid tqdm progress bar interruption by logger's output to console"""
    # see logging.StreamHandler.eval method:
    # https://github.com/python/cpython/blob/d2e2534751fd675c4d5d3adc208bf4fc984da7bf/Lib/logging/__init__.py#L1082-L1091
    # and tqdm.write method:
    # https://github.com/tqdm/tqdm/blob/f86104a1f30c38e6f80bfd8fb16d5fcde1e7749f/tqdm/std.py#L614-L620

    def emit(self, record):
        try:
            msg = self.format(record)
            tqdm.write(msg, end=self.terminator)
        except RecursionError:
            raise
        except Exception:
            self.handleError(record)

测试:

import time

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
log.addHandler(TqdmLoggingHandler())
#   ^-- Assumes this will be the unique handler emitting messages to sys.stdout.
#       If other handlers output to sys.stdout (without tqdm.write),
#       progress bar will be interrupted by those outputs

for i in tqdm(range(20)):
    log.info(f"Looping {i}")
    time.sleep(0.1)
<注意:如果您正在使用Jupyter笔记本工作,进度条将会被中断,并且据我所知没有避免的方法。>

2

一种简单但不太优雅的解决方案是将tqdm对象转换为字符串。之后,您可以记录消息(或按照您的需求处理消息)。"format_dict"属性也可能有用:

from tqdm import tqdm
import time

#loop with progressbar:
it=tqdm(total=10)
for i in range(10):
    time.sleep(0.1)
    it.update(1)

it.close()
print("\n","--"*10)

# Convert tdqm object last output to sting
str_bar_msg = str(it)
print(str_bar_msg)

# See attributes:
print(it.format_dict)

输出:

100%|██████████| 10/10 [00:01<00:00,  8.99it/s]
 --------------------
100%|██████████| 10/10 [00:01<00:00,  8.98it/s]
{'n': 10, 'total': 10, 'elapsed': 1.1145293712615967, 'ncols': None, 'nrows': None, 'prefix': '', 'ascii': False, 'unit': 'it', 'unit_scale': False, 'rate': None, 'bar_format': None, 'postfix': None, 'unit_divisor': 1000, 'initial': 0, 'colour': None}

最好的问候


如果您正在使用Jupyter Notebook,则使用from tqdm.notebook import tqdm - robertspierre
实际上使用from tqdm.auto import tqdm来自动检测是否在Jupyter笔记本中,并使用适当的进度条。 - tbrugere

2

最简单的方法是更改StreamHandler对象的流,例如:

import logging
from tqdm import tqdm, trange
import time

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setStream(tqdm) # <-- important
handler = log.addHandler(handler)

for i in trange(100):
    if i == 50:
        log.info("Half-way there!")
    time.sleep(0.1)

在COLAB中直接执行时,log.info会出现警告,无法正常工作。 - krenerd
log.info("Half-way there!") 替换为 tqdm.write("Half-way there!")。结果是相同的,这意味着问题出在 tqdm 模块本身,因为使用 .write(...) 是官方的打印方法。 - asiloisad

1

新的io处理程序非常有用!

class TqdmToLogger(io.StringIO):
    logger = None
    level = None
    buf = ""

    def __init__(self, logger, level=None, mininterval=5):
        super(TqdmToLogger, self).__init__()
        self.logger = logger
        self.level = level or logging.INFO
        self.mininterval = mininterval
        self.last_time = 0

    def write(self, buf):
        self.buf = buf.strip("\r\n\t ")

    def flush(self):
        if len(self.buf) > 0 and time.time() - self.last_time > self.mininterval:
            self.logger.log(self.level, self.buf)
            self.last_time = time.time()```


# before this line, you need to create logger with file handler
tqdm_out = TqdmToLogger(logger)
tbar = tqdm(sample, total=len(sample), file=tqdm_out)

logger.info("Model Inference.")
for it, batch_data in enumerate(tbar):
   pass

```

目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接