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

Я использую библиотеку 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()

?

5 ответов

Решение

Попробуйте этот контекстный менеджер:

from cStringIO 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 быть списком, а также контекстным менеджером, возможно, более удобно).

В 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 в другой файл или файловый объект.

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

Например, вывод 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 означает, что этот менеджер контекста не подходит для использования в коде библиотеки и большинстве многопоточных приложений. Это также не влияет на вывод подпроцессов. Тем не менее, это все еще полезный подход для многих служебных скриптов.

Этот контекстный менеджер является реентерабельным.

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

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()

Кроме того, опираясь на ответы @kindall и @ForeveWintr, вот класс, который выполняет это. Основное отличие от предыдущих ответов заключается в том, что это фиксирует его как строку , а не как StringIOобъект, с которым гораздо удобнее работать!

      import io
from collections import UserString
from contextlib import redirect_stdout

class capture(UserString, str, redirect_stdout):
    '''
    Captures stdout (e.g., from ``print()``) as a variable.

    Based on ``contextlib.redirect_stdout``, but saves the user the trouble of
    defining and reading from an IO stream. Useful for testing the output of functions
    that are supposed to print certain output.
    '''

    def __init__(self, seq='', *args, **kwargs):
        self._io = io.StringIO()
        UserString.__init__(self, seq=seq, *args, **kwargs)
        redirect_stdout.__init__(self, self._io)
        return

    def __enter__(self, *args, **kwargs):
        redirect_stdout.__enter__(self, *args, **kwargs)
        return self

    def __exit__(self, *args, **kwargs):
        self.data += self._io.getvalue()
        redirect_stdout.__exit__(self, *args, **kwargs)
        return

    def start(self):
        self.__enter__()
        return self

    def stop(self):
        self.__exit__(None, None, None)
        return

Примеры:

      # Using with...as
with capture() as txt1:
    print('Assign these lines')
    print('to a variable')

# Using start()...stop()
txt2 = capture().start()
print('This works')
print('the same way')
txt2.stop()

print('Saved in txt1:')
print(txt1)
print('Saved in txt2:')
print(txt2)

Это реализовано в Sciris как sc.capture() .

На основе kindallа также ForeverWintrответ.

я создаю redirect_stdoutфункция для Python<3.4:

      import io
from contextlib import contextmanager

@contextmanager
def redirect_stdout(f):
    try:
        _stdout = sys.stdout
        sys.stdout = f
        yield
    finally:
        sys.stdout = _stdout


f = io.StringIO()
with redirect_stdout(f):
    do_something()
out = f.getvalue()
Другие вопросы по тегам