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

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

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

1
2
3
4
5
6
7
8
9
from time import sleep
from tqdm import tqdm

def blabla():
  print"Foo blabla"

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

这将创建输出:

1
2
3
4
5
6
7
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]

根据tqdm的文档,方法tqdm.write()提供了一种在不中断显示的进度栏的情况下将消息写入控制台的方法。
因此,此代码段提供了正确的输出:

1
2
3
4
5
6
7
8
9
from time import sleep
from tqdm import tqdm

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

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

看起来像这样:

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

另一方面,存在这种解决方案,它允许通过相当优雅地将sys.stdout重定向到void中来使那些功能静音。
这对于使功能静音非常有效。

由于我仍然希望显示这些功能的消息而又不破坏进度条,因此我尝试通过将sys.stdout重定向到tqdm.write()并依次将tqdm.write()写入旧的sys.stdout来将两种解决方案合并为一个。
结果是代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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.stdout,因此tqdm会感到困惑,因为文件句柄发生了变化。

因此,您只需要显式地将file=sys.stdout指定为tqdm,它将起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 \
)
    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()时,没有无用的\
)。

/ EDIT:实际上,看来您可以在启动tqdm之后执行stdout重定向,只需在tqdm中指定dynamic_ncols=True


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

1
2
3
4
5
6
7
8
9
10
11
12
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参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

要使用它,只需:

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

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

1
2
3
4
5
6
7
8
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):

1
2
3
4
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中的一个简单的相等运算符可修复所有问题。

1
2
3
4
5
6
7
8
9
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

另外,由于打印将换行符传递到sys.stdout(或不取决于用户选择),因此您不希望tqdm自己添加另一个,因此最好设置选项end=''比执行strip在内容上。

该解决方案的优势

用令人惊讶的答案,tqdm(..., file=sys.stdout)用条形图污染您的输出流。通过保留file=sys.stdout(默认值),可以使流保持分离。
有了Conchylicultor和user493630的回答,您才可以打补丁打印。但是,其他系统(例如日志记录)直接流式传输到sys.stdout,因此它们不会通过tqdm.write