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.