Как записать вывод stdout из вызова функции Python?

112

Я использую библиотеку Python, которая что-то делает с объектом

do_something(my_object)

и меняет это. При этом он выводит некоторую статистику на стандартный вывод, и я хотел бы получить эту информацию. Правильным решением было бы изменить, do_something()чтобы вернуть соответствующую информацию,

out = do_something(my_object)

но пройдет некоторое время, прежде чем разработчики займутся do_something()этой проблемой. В качестве обходного пути я подумал о разборе всего, что do_something()записывается в стандартный вывод.

Как я могу захватить вывод stdout между двумя точками кода, например,

start_capturing()
do_something(my_object)
out = end_capturing()

?

Нико Шлёмер
источник
2
возможный дубликат записи результатов dis.dis
Мартейн Питерс
Мой ответ в связанном вопросе применим и здесь.
Мартейн Питерс
Я попытался сделать это один раз, и лучший ответ, который я нашел, был: stackoverflow.com/a/3113913/1330293
elyase
Связанный ответ @elyase - элегантное решение
Пайклер
Смотрите этот ответ .
Мартино

Ответы:

186

Попробуйте этот диспетчер контекста:

from io import StringIO 
import sys

class Capturing(list):
    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self
    def __exit__(self, *args):
        self.extend(self._stringio.getvalue().splitlines())
        del self._stringio    # free up some memory
        sys.stdout = self._stdout

Использование:

with Capturing() as output:
    do_something(my_object)

output теперь список, содержащий строки, напечатанные при вызове функции.

Расширенное использование:

Что может быть неочевидным, так это то, что это можно делать более одного раза, а результаты объединяются:

with Capturing() as output:
    print('hello world')

print('displays on screen')

with Capturing(output) as output:  # note the constructor argument
    print('hello world2')

print('done')
print('output:', output)

Вывод:

displays on screen                     
done                                   
output: ['hello world', 'hello world2']

Обновление : Они добавили redirect_stdout()к contextlibв Python 3.4 (наряду с redirect_stderr()). Таким образом, вы можете использовать io.StringIOэто для достижения аналогичного результата (хотя Capturingсписок, а также диспетчер контекста, возможно, более удобны).

любезный
источник
Спасибо! И спасибо за добавление расширенного раздела ... Изначально я использовал назначение срезов, чтобы вставить захваченный текст в список, затем я ударил себя по голове и использовал .extend()вместо этого, чтобы его можно было использовать конкатентивно, как вы заметили. :-)
kindall
PS Если он будет использоваться повторно, я бы предложил добавить self._stringio.truncate(0)после self.extend()вызова __exit__()метода, чтобы освободить часть памяти, удерживаемой _stringioучастником.
Мартино
25
Отличный ответ, спасибо. Для Python 3 используйте from io import StringIOвместо первой строки в диспетчере контекста.
Wtower
1
Это потокобезопасно? Что произойдет, если какой-то другой поток / вызов использует print () во время выполнения do_something?
Derorrist
1
Этот ответ не будет работать для вывода из общих библиотек C, вместо этого см. Этот ответ .
craymichael
82

В python> = 3.4 contextlib содержит redirect_stdoutдекоратор. Его можно использовать, чтобы ответить на ваш вопрос так:

import io
from contextlib import redirect_stdout

f = io.StringIO()
with redirect_stdout(f):
    do_something(my_object)
out = f.getvalue()

Из документов :

Диспетчер контекста для временного перенаправления sys.stdout в другой файл или файловый объект.

Этот инструмент добавляет гибкости существующим функциям или классам, вывод которых жестко привязан к stdout.

Например, вывод help () обычно отправляется в sys.stdout. Вы можете записать этот вывод в строку, перенаправив вывод на объект io.StringIO:

  f = io.StringIO() 
  with redirect_stdout(f):
      help(pow) 
  s = f.getvalue()

Чтобы отправить вывод help () в файл на диске, перенаправьте вывод в обычный файл:

 with open('help.txt', 'w') as f:
     with redirect_stdout(f):
         help(pow)

Чтобы отправить вывод help () в sys.stderr:

with redirect_stdout(sys.stderr):
    help(pow)

Обратите внимание, что глобальный побочный эффект для sys.stdout означает, что этот диспетчер контекста не подходит для использования в коде библиотеки и в большинстве многопоточных приложений. Это также не влияет на вывод подпроцессов. Тем не менее, это все еще полезный подход для многих служебных скриптов.

Этот диспетчер контекста является реентерабельным.

ForeverWintr
источник
когда попробовал f = io.StringIO() with redirect_stdout(f): logger = getLogger('test_logger') logger.debug('Test debug message') out = f.getvalue() self.assertEqual(out, 'DEBUG:test_logger:Test debug message'). AssertionError: '' != 'Test debug message'
Выдает
Это означает, что я сделал что-то не так, или он не смог поймать журнал stdout.
Эзиз Дурдыев
@EzizDurdyyev, logger.debugпо умолчанию не пишет в stdout. Если вы замените вызов журнала на, print()вы должны увидеть сообщение.
ForeverWintr
Да, я знаю, но я сделать это написать на стандартный вывод так: stream_handler = logging.StreamHandler(sys.stdout). И добавьте этот обработчик в мой регистратор. поэтому он должен писать в stdout иredirect_stdout ловить его, верно?
Эзиз Дурдыев
Я подозреваю, что проблема в том, как вы настроили регистратор. Я бы проверил, что он выводится на стандартный вывод без redirect_stdout. Если это так, возможно, буфер не очищается до выхода из диспетчера контекста.
ForeverWintr
0

Вот асинхронное решение с использованием файловых каналов.

import threading
import sys
import os

class Capturing():
    def __init__(self):
        self._stdout = None
        self._stderr = None
        self._r = None
        self._w = None
        self._thread = None
        self._on_readline_cb = None

    def _handler(self):
        while not self._w.closed:
            try:
                while True:
                    line = self._r.readline()
                    if len(line) == 0: break
                    if self._on_readline_cb: self._on_readline_cb(line)
            except:
                break

    def print(self, s, end=""):
        print(s, file=self._stdout, end=end)

    def on_readline(self, callback):
        self._on_readline_cb = callback

    def start(self):
        self._stdout = sys.stdout
        self._stderr = sys.stderr
        r, w = os.pipe()
        r, w = os.fdopen(r, 'r'), os.fdopen(w, 'w', 1)
        self._r = r
        self._w = w
        sys.stdout = self._w
        sys.stderr = self._w
        self._thread = threading.Thread(target=self._handler)
        self._thread.start()

    def stop(self):
        self._w.close()
        if self._thread: self._thread.join()
        self._r.close()
        sys.stdout = self._stdout
        sys.stderr = self._stderr

Пример использования:

from Capturing import *
import time

capturing = Capturing()

def on_read(line):
    # do something with the line
    capturing.print("got line: "+line)

capturing.on_readline(on_read)
capturing.start()
print("hello 1")
time.sleep(1)
print("hello 2")
time.sleep(1)
print("hello 3")
capturing.stop()
miXo
источник