通过 tqdm.write() 重定向 python 脚本中的打印命令

Redirect print command in python script through tqdm.write()

我在 Python 中使用 tqdm 在我们的脚本中显示控制台进度条。 但是,我还必须调用将 print 消息发送到控制台且我无法更改的函数。 通常,在控制台中显示进度条的同时写入控制台会使显示混乱,如下所示:

from time import sleep
from tqdm import tqdm

def blabla():
  print "Foo blabla"

for k in tqdm(range(3)):
  blabla()
  sleep(.5)

这将创建输出:

0%|                                           | 0/3 [00:00<?, ?it/s]Foo
blabla
33%|###########6                       | 1/3 [00:00<00:01,  2.00it/s]Foo
blabla
67%|#######################3           | 2/3 [00:01<00:00,  2.00it/s]Foo
blabla
100%|###################################| 3/3 [00:01<00:00,  2.00it/s]

According to the documentation of tqdm 方法 tqdm.write() 提供了一种在不破坏显示的进度条的情况下将消息写入控制台的方法。 因此,此代码段提供了正确的输出:

from time import sleep
from tqdm import tqdm

def blabla():
  tqdm.write("Foo blabla")

for k in tqdm(range(3)):
  blabla()
  sleep(.5)

看起来像这样:

Foo blabla
Foo blabla
Foo blabla
100%|###################################| 3/3 [00:01<00:00,  1.99it/s]

另一方面,solution which permits to silence those functions 非常优雅地将 sys.stdout 重定向到空白中。 这非常适合静音功能。

由于我仍然想在不破坏进度条的情况下显示来自这些函数的消息,因此我尝试通过将 sys.stdout 重定向到 tqdm.write() 将两个解决方案合并为一个,然后让 tqdm.write() 写给 old sys.stdout。 这导致片段:

from time import sleep

import contextlib
import sys

from tqdm import tqdm

class DummyFile(object):
  file = None
  def __init__(self, file):
    self.file = file

  def write(self, x):
    tqdm.write(x, file=self.file)

@contextlib.contextmanager
def nostdout():
    save_stdout = sys.stdout
    sys.stdout = DummyFile(save_stdout)
    yield
    sys.stdout = save_stdout

def blabla():
  print "Foo blabla"

for k in tqdm(range(3)):
  with nostdout():
    blabla()
    sleep(.5)

但是,这实际上会像以前一样产生更混乱的输出:

0%|                                           | 0/3 [00:00<?, ?it/s]Foo
blabla


33%|###########6                       | 1/3 [00:00<00:01,  2.00it/s]Foo
blabla


67%|#######################3           | 2/3 [00:01<00:00,  2.00it/s]Foo
blabla


100%|###################################| 3/3 [00:01<00:00,  2.00it/s]

仅供参考:在 DummyFile.write() 中调用 tqdm.write(..., end="") 会产生与第一个仍然混乱的输出相同的结果。

我不明白为什么这行不通,因为 tqdm.write() 应该管理在写入消息之前清除进度条然后重写进度条。

我错过了什么?

重定向 sys.stdout 总是很棘手,当两个应用程序同时摆弄它时,它会变成一场噩梦。

这里的技巧是 tqdm 默认打印到 sys.stderr,而不是 sys.stdout。通常,tqdm 对这两个特殊通道有一个反混淆策略,但由于您正在重定向 sys.stdouttqdm 会因为文件句柄更改而混淆。

因此,您只需要明确指定 file=sys.stdouttqdm 就可以了:

from time import sleep

import contextlib
import sys

from tqdm import tqdm

class DummyFile(object):
  file = None
  def __init__(self, file):
    self.file = file

  def write(self, x):
    # Avoid print() second call (useless \n)
    if len(x.rstrip()) > 0:
        tqdm.write(x, file=self.file)

@contextlib.contextmanager
def nostdout():
    save_stdout = sys.stdout
    sys.stdout = DummyFile(sys.stdout)
    yield
    sys.stdout = save_stdout

def blabla():
  print("Foo blabla")

# tqdm call to sys.stdout must be done BEFORE stdout redirection
# and you need to specify sys.stdout, not sys.stderr (default)
for _ in tqdm(range(3), file=sys.stdout):
    with nostdout():
        blabla()
        sleep(.5)

print('Done!')

我还添加了一些技巧来使输出更好(例如,在不使用 end='' 的情况下使用 print() 时没有无用的 \n)。

/EDIT:事实上你似乎可以在启动 tqdm 后进行 stdout 重定向,你只需要在 tqdm.[=27 中指定 dynamic_ncols=True =]

这可能是不好的方法,但我更改了内置打印功能。

import inspect
import tqdm
# store builtin print
old_print = print
def new_print(*args, **kwargs):
    # if tqdm.tqdm.write raises error, use builtin print
    try:
        tqdm.tqdm.write(*args, **kwargs)
    except:
        old_print(*args, ** kwargs)
# globaly replace print with new_print
inspect.builtins.print = new_print

通过混合 user493630 和冗长的答案,我创建了这个上下文管理器,它避免了必须使用 tqdmfile=sys.stdout 参数。

import inspect
import contextlib
import tqdm

@contextlib.contextmanager
def redirect_to_tqdm():
    # Store builtin print
    old_print = print
    def new_print(*args, **kwargs):
        # If tqdm.tqdm.write raises error, use builtin print
        try:
            tqdm.tqdm.write(*args, **kwargs)
        except:
            old_print(*args, ** kwargs)

    try:
        # Globaly replace print with new_print
        inspect.builtins.print = new_print
        yield
    finally:
        inspect.builtins.print = old_print

要使用它,只需:

for i in tqdm.tqdm(range(100)):
    with redirect_to_tqdm():
        time.sleep(.1)
        print(i)

为了进一步简化,可以将代码包装在一个新函数中:

def tqdm_redirect(*args, **kwargs):
    with redirect_to_tqdm():
        for x in tqdm.tqdm(*args, **kwargs):
            yield x

for i in tqdm_redirect(range(20)):
    time.sleep(.1)
    print(i)

OP的解决方案几乎是正确的。 tqdm 库中弄乱你的输出的测试是这个 (https://github.com/tqdm/tqdm/blob/master/tqdm/_tqdm.py#L546-L549):

if hasattr(inst, "start_t") and (inst.fp == fp or all(
           f in (sys.stdout, sys.stderr) for f in (fp, inst. 
    inst.clear(nolock=True)
    inst_cleared.append(inst)

tqdm.write 正在测试您提供的文件,以查看要打印的文本和潜在的 tqdm 条之间是否存在冲突的风险。在您的情况下,stdout 和 stderr 在终端中混合在一起,因此会发生冲突。为了解决这个问题,当测试通过时,tqdm 清除条形图,打印文本并在之后绘制条形图。

这里,测试fp == sys.stdout失败,因为sys.stdout变成了DummyFile,而fp才是真正的sys.stdout,所以清理行为没有开启。 DummyFile 中的一个简单的相等运算符解决了所有问题。

class DummyFile(object):
    def __init__(self, file):
        self.file = file

    def write(self, x):
        tqdm.write(x, end="", file=self.file)

    def __eq__(self, other):
        return other is self.file

此外,由于 print 将换行符传递给 sys.stdout(或不传递,这取决于用户的选择),您不希望 tqdm 自己添加另一个换行符,因此最好设置选项 end='' 而不是对内容执行 strip

此解决方案的优点

对于 gaborous 的回答,tqdm(..., file=sys.stdout) 会用条块污染您的输出流。通过保持 file=sys.stdout(默认),您可以将流分开。
使用 Conchylicultor 和 user493630 的答案,您只能打印补丁。但是,其他系统(例如日志记录)直接流向 sys.stdout,因此它们不会经过 tqdm.write.