Comment capturer la sortie stdout d'un appel de fonction Python?

112

J'utilise une bibliothèque Python qui fait quelque chose sur un objet

do_something(my_object)

et le change. Ce faisant, il imprime des statistiques sur stdout, et j'aimerais avoir une idée de ces informations. La bonne solution serait de changer do_something()pour renvoyer les informations pertinentes,

out = do_something(my_object)

mais il faudra un certain temps avant que les développeurs do_something()se penchent sur ce problème. Pour contourner le problème, j'ai pensé à analyser tout ce qui do_something()écrit sur stdout.

Comment puis-je capturer la sortie stdout entre deux points dans le code, par exemple,

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

?

Nico Schlömer
la source
2
duplication possible de la capture des résultats dis.dis
Martijn Pieters
Ma réponse à la question liée s'applique également ici.
Martijn Pieters
J'ai essayé de le faire une fois et la meilleure réponse que j'ai trouvée était: stackoverflow.com/a/3113913/1330293
elyase
La réponse liée @elyase est une solution élégante
Pykler
Voyez cette réponse .
martineau

Réponses:

184

Essayez ce gestionnaire de contexte:

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

Usage:

with Capturing() as output:
    do_something(my_object)

output est maintenant une liste contenant les lignes imprimées par l'appel de fonction.

Utilisation avancée:

Ce qui n'est peut-être pas évident, c'est que cela peut être fait plus d'une fois et les résultats concaténés:

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)

Production:

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

Mise à jour : ils ont été ajoutés redirect_stdout()à contextlibPython 3.4 (avec redirect_stderr()). Vous pouvez donc l'utiliser io.StringIOpour obtenir un résultat similaire (bien Capturingqu'être une liste et un gestionnaire de contexte soit sans doute plus pratique).

kindall
la source
Merci! Et merci d'avoir ajouté la section avancée ... J'ai initialement utilisé une affectation de tranche pour coller le texte capturé dans la liste, puis je me suis amusé dans la tête et je l'ai utilisé à la .extend()place afin qu'il puisse être utilisé de manière concaténante, comme vous l'avez remarqué. :-)
kindall
PS Si cela doit être utilisé à plusieurs reprises, je suggère d'ajouter un self._stringio.truncate(0)après l' self.extend()appel dans la __exit__()méthode pour libérer une partie de la mémoire détenue par le _stringiomembre.
martineau
25
Excellente réponse, merci. Pour Python 3, utilisez à la from io import StringIOplace de la première ligne dans le gestionnaire de contexte.
Wtower
1
Est-ce thread-safe? Que se passe-t-il si un autre thread / appel utilise print () pendant l'exécution de do_something?
Derorrist
1
Cette réponse ne fonctionnera pas pour la sortie des bibliothèques partagées C, voyez plutôt cette réponse .
craymichael
82

En python> = 3.4, contextlib contient un redirect_stdoutdécorateur. Il peut être utilisé pour répondre à votre question comme ceci:

import io
from contextlib import redirect_stdout

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

À partir de la documentation :

Gestionnaire de contexte pour rediriger temporairement sys.stdout vers un autre fichier ou objet semblable à un fichier.

Cet outil ajoute de la flexibilité aux fonctions ou classes existantes dont la sortie est câblée à stdout.

Par exemple, la sortie de help () est normalement envoyée à sys.stdout. Vous pouvez capturer cette sortie dans une chaîne en redirigeant la sortie vers un objet io.StringIO:

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

Pour envoyer la sortie de help () vers un fichier sur le disque, redirigez la sortie vers un fichier normal:

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

Pour envoyer la sortie de help () à sys.stderr:

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

Notez que l'effet secondaire global sur sys.stdout signifie que ce gestionnaire de contexte n'est pas adapté à une utilisation dans le code de bibliothèque et la plupart des applications threadées. Il n'a également aucun effet sur la sortie des sous-processus. Cependant, c'est toujours une approche utile pour de nombreux scripts utilitaires.

Ce gestionnaire de contexte est réentrant.

ForeverWintr
la source
une fois essayé 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'). Cela me donne une erreur:AssertionError: '' != 'Test debug message'
Eziz Durdyyev
ce qui signifie que j'ai fait quelque chose de mal ou qu'il n'a pas pu attraper le journal stdout.
Eziz Durdyyev
@EzizDurdyyev, logger.debugn'écrit pas dans stdout par défaut. Si vous remplacez votre appel de journal par, print()vous devriez voir le message.
ForeverWintr
Ouais, je sais, mais je le fais écrire à stdout comme ceci: stream_handler = logging.StreamHandler(sys.stdout). Et ajoutez ce gestionnaire à mon enregistreur. il devrait donc écrire sur stdout et l' redirect_stdoutattraper, non?
Eziz Durdyyev
Je soupçonne que le problème vient de la façon dont vous avez configuré votre enregistreur. Je vérifierais qu'il imprime sur stdout sans le redirect_stdout. Si c'est le cas, peut-être que le tampon n'est pas vidé jusqu'à ce que le gestionnaire de contexte se termine.
ForeverWintr
0

Voici une solution asynchrone utilisant des canaux de fichiers.

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

Exemple d'utilisation:

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
la source