web-dev-qa-db-fra.com

Rediriger la commande d'impression dans un script python via tqdm.write ()

J'utilise tqdm en Python pour afficher la barre de progression de la console dans nos scripts. Cependant, je dois appeler des fonctions qui print envoient également des messages à la console et que je ne peux pas modifier. Dans. En général, écrire sur la console tout en affichant des barres de progression dans la console perturbe l'affichage de la manière suivante:

from time import sleep
from tqdm import tqdm

def blabla():
  print "Foo blabla"

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

Cela crée la sortie:

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]

Selon la documentation de tqdm , la méthode tqdm.write() fournit un moyen d'écrire des messages sur la console sans casser les barres de progression affichées. Ainsi, le bon résultat est fourni par cet extrait:

from time import sleep
from tqdm import tqdm

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

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

Et ressemble à ceci:

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

D'autre part, il existe cette solution qui permet de réduire ces fonctions au silence en redirigeant assez élégamment sys.stdout dans le vide. Ceci fonctionne parfaitement bien pour faire taire les fonctions.

Puisque je veux néanmoins afficher les messages de ces fonctions sans casser les barres de progression, j’ai essayé de fusionner les deux solutions en redirigeant sys.stdout vers tqdm.write() et, à son tour, en laissant tqdm.write() écrire dans le old sys.stdout. .____.] Il en résulte l'extrait de code:

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)

Cependant, cela crée en réalité une sortie encore plus confuse que précédemment:

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]

FYI: appeler tqdm.write(..., end="") dans DummyFile.write() crée le même résultat que la première sortie qui est toujours foirée.

Je ne comprends pas pourquoi cela ne fonctionnerait pas, car tqdm.write() est supposé gérer l'effacement de la barre de progression avant d'écrire le message, puis de réécrire la barre de progression.

Qu'est-ce que je rate?

11

La redirection de sys.stdout est toujours délicate, et cela devient un cauchemar lorsque deux applications la manipulent en même temps.

Ici, l’astuce est que tqdm par défaut s’imprime en sys.stderr et non pas sys.stdout. Normalement, tqdm a une stratégie anti-mélange pour ces deux canaux spéciaux, mais puisque vous redirigez sys.stdout, tqdm devient confus parce que le descripteur de fichier change.

Ainsi, il vous suffit de spécifier explicitement file=sys.stdout à tqdm et cela fonctionnera:

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

J'ai également ajouté quelques astuces supplémentaires pour rendre la sortie plus agréable (par exemple, pas de \n inutile lors de l'utilisation de print() sans end='').

/ EDIT: en fait, il semble que vous puissiez faire la redirection stdout après avoir démarré tqdm, il vous suffit de spécifier dynamic_ncols=True dans tqdm.

13
gaborous

C'est peut-être la mauvaise façon, mais je change la fonction d'impression intégrée.

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
6
user493630

En mélangeant les réponses user493630 et gaborous, j'ai créé ce gestionnaire de contexte qui évite de devoir utiliser le paramètre 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

Pour l'utiliser, simplement:

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

Pour simplifier davantage, il est possible d’envelopper le code dans une nouvelle fonction:

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)
2
Conchylicultor

La solution de l'OP est presque correcte. Le test de la bibliothèque tqdm qui perturbe votre sortie est celui-ci ( 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 teste le fichier que vous avez fourni pour voir s’il existe un risque de collision entre le texte à imprimer et les barres tqdm potentielles. Dans votre cas, stdout et stderr sont mélangés dans le terminal, ce qui entraîne une collision. Pour contrer cela, lorsque le test est réussi, tqdm efface les barres, imprime le texte et dessine les barres après. 

Ici, le test fp == sys.stdout échoue car sys.stdout est devenu DummyFile et fp est le sys.stdout réel. Le comportement de nettoyage n'est donc pas activé. Un opérateur d'égalité simple dans DummyFile corrige tout.

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

De plus, comme print transmet une nouvelle ligne à sys.stdout (ou pas, en fonction du choix de l'utilisateur), vous ne voulez pas que tqdm en ajoute un autre lui-même. Il est donc préférable de définir l'option end='' plutôt que d'exécuter strip sur le contenu.

Avantages de cette solution

Avec la réponse de gaborous, tqdm(..., file=sys.stdout) pollue votre flux de sortie avec des morceaux de barre. En conservant file=sys.stdout (valeur par défaut), vous séparez vos flux.
Avec les réponses de Conchylicultor et de user493630, vous ne corrigez que l'impression. Cependant, d'autres systèmes tels que la journalisation transmettent directement à sys.stdout, de sorte qu'ils ne passent pas par tqdm.write.

0
Perceval W