Перенаправить команду печати в python script через tqdm.write()

Я использую tqdm в Python для отображения консоли-progressbars в наших скриптах. Тем не менее, я должен вызывать функции, которые 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]

В соответствии с документацией 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]

С другой стороны, существует это решение, которое позволяет отключить эти функции, довольно элегантно перенаправляя 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]

FYI: вызов tqdm.write(..., end="") внутри DummyFile.write() создает тот же результат, что и первый вывод, который все еще запутан.

Я не понимаю, почему это не сработает, так как tqdm.write() должен управлять очисткой индикатора выполнения перед записью сообщения, а затем переписывать индикатор выполнения.

Что мне не хватает?

Ответ 1

Переадресация sys.stdout всегда сложна, и она становится кошмаром, когда одновременно работают два приложения.

Вот трюк в том, что tqdm по умолчанию печатает на sys.stderr, а не sys.stdout. Как правило, tqdm имеет стратегию против смешивания для этих двух специальных каналов, но поскольку вы перенаправляете sys.stdout, tqdm запутывается, потому что изменяется дескриптор файла.

Таким образом, вам просто нужно явно указать file=sys.stdout на tqdm, и он будет работать:

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

Я добавил еще несколько трюков, чтобы сделать вывод более приятным (например, без использования \n при использовании print() без end='').

/EDIT: на самом деле вы можете сделать перенаправление stdout после запуска tqdm, вам просто нужно указать dynamic_ncols=True в tqdm.

Ответ 2

Это может быть плохой способ, но я меняю встроенную функцию печати. ​​

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

Ответ 3

Смешивая user493630 и gaborous ответы, я создал этот менеджер контекста, который не должен использовать параметр file=sys.stdout 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

Чтобы использовать его, просто:

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)

Ответ 4

ОП решение почти правильное. Вот тест в библиотеке 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 в sys.stdout (или не зависит от выбора пользователя), вы не хотите, чтобы tqdm самостоятельно добавлял другую, поэтому лучше установить опцию end='' чем выполнять strip на содержимое.

Преимущества этого решения

С громким ответом tqdm(..., file=sys.stdout) загрязняет ваш поток вывода кусками бара. Сохраняя file=sys.stdout (по умолчанию), вы разделяете свои потоки.
С ответами Conchylicultor и user493630 вы печатаете только патч. Однако другие системы, такие как ведение журнала, напрямую передаются в sys.stdout, поэтому они не проходят через tqdm.write.