Як зафіксувати вихід stdout з виклику функції Python?


112

Я використовую бібліотеку Python, яка робить щось із об’єктом

do_something(my_object)

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

out = do_something(my_object)

але пройде ще деякий час, перш ніж чорти do_something()дістануться до цього питання. Як вирішення, я подумав про розбір того, що do_something()пише для stdout.

Як я можу зафіксувати висновок stdout між двома точками в коді, наприклад,

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

?



Моя відповідь на пов'язане питання стосується і тут.
Martijn Pieters

Я намагався зробити це один раз і краща відповідь я знайшов: stackoverflow.com/a/3113913/1330293
elyase

@elyase пов'язана відповідь - це елегантне рішення
Pykler

Дивіться цю відповідь .
мартіно

Відповіді:


183

Спробуйте цей контекстний менеджер:

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учасник.
martineau

25
Чудова відповідь, дякую. Для Python 3 використовуйте from io import StringIOзамість першого рядка в менеджері контексту.
Wtower

1
Чи безпечна ця нитка? Що трапиться, якщо якийсь інший потік / виклик використовує print (), коли do_something працює?
Дерорист

1
Ця відповідь не працюватиме на вихід з C поділюваних бібліотек см ця відповідь замість.
craymichael

81

У 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 означає, що цей контекстний менеджер не підходить для використання в бібліотечному коді та більшості потокових програм. Це також не впливає на вихід підпроцесів. Однак це все ще корисний підхід для багатьох скриптів утиліти.

Цей менеджер контексту є ретентом.


коли судили 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повинен його ловити, правда?
Езіз Дурдиєв

Я підозрюю, що проблема пов’язана з тим, як ви налаштували свій реєстратор. Я би переконався, що він друкує до 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()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.