Redirigir el comando de impresión en el script de python a través de tqdm.write ()

Estoy usando tqdm en Python para mostrar las tqdm de progreso de la consola en nuestros scripts. Sin embargo, tengo que llamar a funciones que print mensajes a la consola y que no puedo cambiar. En general, escribir en la consola mientras se muestran las barras de progreso en la consola desordena la pantalla de la siguiente manera:

 from time import sleep from tqdm import tqdm def blabla(): print "Foo blabla" for k in tqdm(range(3)): blabla() sleep(.5) 

Esto crea la salida:

 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] 

De acuerdo con la documentación de tqdm el método tqdm.write() proporciona un medio para escribir mensajes en la consola sin romper las barras de progreso mostradas. Por lo tanto, la salida correcta es proporcionada por este fragmento:

 from time import sleep from tqdm import tqdm def blabla(): tqdm.write("Foo blabla") for k in tqdm(range(3)): blabla() sleep(.5) 

Y se ve así:

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

Por otro lado, existe esta solución que permite silenciar esas funciones redirigiendo de forma bastante elegante sys.stdout al vacío. Esto funciona perfectamente bien para silenciar las funciones.

Como quiero mostrar los mensajes de estas funciones sin romper las barras de progreso, intenté fusionar ambas soluciones en una redirigiendo sys.stdout a tqdm.write() y, a su vez, dejando que tqdm.write() escriba a viejos sys.stdout . Esto se traduce en el fragmento:

 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) 

Sin embargo, esto en realidad crea una salida aún más desordenada como antes:

 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] 

Para su información: llamar a tqdm.write(..., end="") dentro de DummyFile.write() crea el mismo resultado que la primera salida que todavía está desordenada.

No puedo entender por qué esto no funcionaría, ya que se supone que tqdm.write() gestiona el borrado de la barra de progreso antes de escribir el mensaje y luego volver a escribir la barra de progreso.

¿Qué me estoy perdiendo?

La redirección de sys.stdout siempre es complicada, y se convierte en una pesadilla cuando dos aplicaciones están jugando al mismo tiempo.

Aquí el truco es que tqdm imprime de forma predeterminada a sys.stderr , no a sys.stdout . Normalmente, tqdm tiene una estrategia anti-mixup para estos dos canales especiales, pero como está redirigiendo sys.stdout , tqdm se confunde debido a que el controlador de archivos cambia.

Por lo tanto, solo necesita especificar explícitamente file=sys.stdout a tqdm y funcionará:

 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!') 

También agregué algunos trucos más para mejorar la salida (por ejemplo, no es inútil \n cuando se usa print() sin end='' ).

/ EDIT: de hecho parece que puedes hacer la redirección de la salida tqdm después de iniciar tqdm , solo necesitas especificar dynamic_ncols=True en tqdm .

Puede que sea la mala manera, pero cambio la función de impresión incorporada.

 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 

Al mezclar las respuestas de user493630 y gaborous, creé este administrador de contexto que evita tener que usar el parámetro file=sys.stdout de tqdm .

 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 

Para usarlo, simplemente:

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

Para simplificar aún más, es posible envolver el código en una nueva función:

 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) 

La solución de OP es casi correcta. La prueba en la biblioteca tqdm que confunde tu salida es la siguiente ( 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 está probando el archivo que proporcionó para ver si existe riesgo de colisión entre el texto para imprimir y las barras de tqdm potenciales. En su caso, stdout y stderr se mezclan en el terminal, por lo que hay una colisión. Para contrarrestar esto, cuando la prueba pasa, tqdm borra las barras, imprime el texto y dibuja las barras después.

Aquí, la prueba fp == sys.stdout falla porque sys.stdout convirtió en DummyFile , y fp es sys.stdout real, por lo que el comportamiento de limpieza no está habilitado. Un simple operador de igualdad en DummyFile arregla todo.

 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 

Además, dado que la impresión pasa una nueva línea a sys.stdout (o no dependiendo de la opción del usuario), no desea que tqdm agregue otra por su cuenta, por lo que es mejor configurar la opción end='' que realizar una strip en el contenido.

Ventajas de esta solución.

Con la respuesta de tqdm(..., file=sys.stdout) , tqdm(..., file=sys.stdout) contamina su flujo de salida con piezas de barra. Al mantener file=sys.stdout (predeterminado), mantiene sus flujos separados.
Con las respuestas de Conchylicultor y user493630, solo puede imprimir parches. Sin embargo, otros sistemas como el registro directamente se transmiten a sys.stdout, por lo que no pasan por tqdm.write .