Come catturare l'output stdout da una chiamata di funzione Python?


112

Sto usando una libreria Python che fa qualcosa su un oggetto

do_something(my_object)

e lo cambia. Mentre lo fa, stampa alcune statistiche su stdout e mi piacerebbe avere una presa su queste informazioni. La soluzione corretta sarebbe cambiare do_something()per restituire le informazioni pertinenti,

out = do_something(my_object)

ma ci vorrà un po 'prima che gli sviluppatori arrivino do_something()a questo problema. Come soluzione alternativa, ho pensato di analizzare tutto ciò che do_something()scrive su stdout.

Come posso catturare l'output stdout tra due punti nel codice, ad esempio,

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

?


2
possibile duplicato della cattura dei risultati di dis.dis
Martijn Pieters

La mia risposta nella domanda collegata si applica anche qui.
Martijn Pieters

Ho cercato di farlo una sola volta e la migliore risposta che ho trovato era: stackoverflow.com/a/3113913/1330293
elyase

La risposta collegata a @elyase è una soluzione elegante
Pykler

Vedi questa risposta .
martineau

Risposte:


183

Prova questo gestore di contesto:

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

Uso:

with Capturing() as output:
    do_something(my_object)

output è ora un elenco contenente le righe stampate dalla chiamata alla funzione.

Utilizzo avanzato:

Ciò che potrebbe non essere ovvio è che questo può essere fatto più di una volta e i risultati concatenati:

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)

Produzione:

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

Aggiornamento : Hanno aggiunto redirect_stdout()di contextlibin Python 3.4 (insieme redirect_stderr()). Quindi potresti usarlo io.StringIOper ottenere un risultato simile (sebbene Capturingessere un elenco oltre che un gestore di contesto sia probabilmente più conveniente).


Grazie! E grazie per aver aggiunto la sezione avanzata ... Inizialmente ho usato un'assegnazione di slice per inserire il testo catturato nell'elenco, poi mi sono messo in testa e l'ho usato .extend()invece in modo che potesse essere usato in modo concatenato, proprio come hai notato. :-)
kindall

PS Se verrà utilizzato ripetutamente, suggerirei di aggiungere un self._stringio.truncate(0)dopo la self.extend()chiamata nel __exit__()metodo per liberare parte della memoria detenuta dal _stringiomembro.
martineau

25
Ottima risposta, grazie. Per Python 3, usa from io import StringIOinvece della prima riga nel gestore di contesto.
Wtower

1
È thread-safe? Cosa succede se qualche altro thread / chiamata usa print () mentre do_something è in esecuzione?
Derorrist

1
Questa risposta non funzionerà per l'output dalle librerie condivise C, vedere invece questa risposta .
craymichael

81

In python> = 3.4, contextlib contiene un redirect_stdoutdecoratore. Può essere usato per rispondere alla tua domanda in questo modo:

import io
from contextlib import redirect_stdout

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

Dai documenti :

Gestore di contesto per reindirizzare temporaneamente sys.stdout a un altro file o oggetto simile a un file.

Questo strumento aggiunge flessibilità alle funzioni o classi esistenti il ​​cui output è cablato a stdout.

Ad esempio, l'output di help () viene normalmente inviato a sys.stdout. Puoi catturare quell'output in una stringa reindirizzando l'output a un oggetto io.StringIO:

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

Per inviare l'output di help () a un file su disco, reindirizza l'output a un file normale:

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

Per inviare l'output di help () a sys.stderr:

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

Si noti che l'effetto collaterale globale su sys.stdout significa che questo gestore di contesto non è adatto per l'uso nel codice della libreria e nella maggior parte delle applicazioni con thread. Inoltre, non ha alcun effetto sull'output dei sottoprocessi. Tuttavia, è ancora un approccio utile per molti script di utilità.

Questo gestore di contesto è rientrante.


quando provato 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'). Mi dà un errore:AssertionError: '' != 'Test debug message'
Eziz Durdyyev

il che significa che ho fatto qualcosa di sbagliato o non è stato possibile catturare il registro stdout.
Eziz Durdyyev

@EzizDurdyyev, logger.debugnon scrive su stdout per impostazione predefinita. Se sostituisci la tua chiamata di registro con print()dovresti vedere il messaggio.
ForeverWintr

Sì, lo so, ma io ce la fanno scrivere su stdout in questo modo: stream_handler = logging.StreamHandler(sys.stdout). E aggiungi quel gestore al mio logger. quindi dovrebbe scrivere su stdout e redirect_stdoutdovrebbe prenderlo, giusto?
Eziz Durdyyev

Sospetto che il problema riguardi il modo in cui hai configurato il tuo logger. Verificherei che stampa su stdout senza redirect_stdout. Se lo fa, forse il buffer non viene svuotato fino a quando il gestore contesto non esce.
ForeverWintr

0

Ecco una soluzione asincrona che utilizza file pipe.

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

Utilizzo di esempio:

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()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.