supposons qu'il y ait un script faisant quelque chose comme ceci:
# module writer.py
import sys
def write():
sys.stdout.write("foobar")
Supposons maintenant que je veuille capturer le résultat de la fonction write
et le stocker dans une variable pour un traitement ultérieur. La solution naïve était:
# module mymodule.py
from writer import write
out = write()
print out.upper()
Mais ça ne marche pas. Je propose une autre solution et cela fonctionne, mais s'il vous plaît, laissez-moi savoir s'il existe un meilleur moyen de résoudre le problème. Merci
import sys
from cStringIO import StringIO
# setup the environment
backup = sys.stdout
# ####
sys.stdout = StringIO() # capture output
write()
out = sys.stdout.getvalue() # release output
# ####
sys.stdout.close() # close the stream
sys.stdout = backup # restore original stdout
print out.upper() # post processing
Définir stdout
est un moyen raisonnable de le faire. Une autre consiste à l'exécuter en tant que processus différent:
import subprocess
proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()
Voici une version de gestionnaire de contexte de votre code. Cela donne une liste de deux valeurs; le premier est stdout, le second est stderr.
import contextlib
@contextlib.contextmanager
def capture():
import sys
from cStringIO import StringIO
oldout,olderr = sys.stdout, sys.stderr
try:
out=[StringIO(), StringIO()]
sys.stdout,sys.stderr = out
yield out
finally:
sys.stdout,sys.stderr = oldout, olderr
out[0] = out[0].getvalue()
out[1] = out[1].getvalue()
with capture() as out:
print 'hi'
Pour les futurs visiteurs: Python 3.4 contextlib fournit cela directement (voir aide de Python contextlib) ) via le redirect_stdout
gestionnaire de contexte:
from contextlib import redirect_stdout
import io
f = io.StringIO()
with redirect_stdout(f):
help(pow)
s = f.getvalue()
Ou peut-être utiliser des fonctionnalités déjà présentes ...
from IPython.utils.capture import capture_output
with capture_output() as c:
print('some output')
c()
print c.stdout
Ceci est la contrepartie décoratrice de mon code original.
writer.py
reste le même:
import sys
def write():
sys.stdout.write("foobar")
mymodule.py
légèrement modifié
from writer import write as _write
from decorators import capture
@capture
def write():
return _write()
out = write()
# out post processing...
Et voici le décorateur:
def capture(f):
"""
Decorator to capture standard output
"""
def captured(*args, **kwargs):
import sys
from cStringIO import StringIO
# setup the environment
backup = sys.stdout
try:
sys.stdout = StringIO() # capture output
f(*args, **kwargs)
out = sys.stdout.getvalue() # release output
finally:
sys.stdout.close() # close the stream
sys.stdout = backup # restore original stdout
return out # captured output wrapped in a string
return captured
À partir de Python 3, vous pouvez également utiliser sys.stdout.buffer.write()
pour écrire des chaînes d'octets (déjà) sur stdout (voir stdout dans Python 3 ). Lorsque vous faites cela, l'approche simple StringIO
ne fonctionne pas car ni sys.stdout.encoding
Ni sys.stdout.buffer
Ne seraient disponibles.
En commençant par Python 2.6, vous pouvez utiliser l'API TextIOBase
, qui inclut les attributs manquants:
import sys
from io import TextIOWrapper, BytesIO
# setup the environment
old_stdout = sys.stdout
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)
# do some writing (indirectly)
write("blub")
# get output
sys.stdout.seek(0) # jump to the start
out = sys.stdout.read() # read output
# restore stdout
sys.stdout.close()
sys.stdout = old_stdout
# do stuff with the output
print(out.upper())
Cette solution fonctionne pour Python 2> = 2.6 et Python 3. Veuillez noter que notre sys.stdout.write()
n'accepte que les chaînes unicode et que sys.stdout.buffer.write()
n'accepte que chaînes d'octets. Ce n'est peut-être pas le cas pour l'ancien code, mais c'est souvent le cas pour le code conçu pour être exécuté sur Python 2 et 3 sans modification.
Si vous devez prendre en charge un code qui envoie des chaînes d'octets à stdout directement sans utiliser stdout.buffer, vous pouvez utiliser cette variante:
class StdoutBuffer(TextIOWrapper):
def write(self, string):
try:
return super(StdoutBuffer, self).write(string)
except TypeError:
# redirect encoded byte strings directly to buffer
return super(StdoutBuffer, self).buffer.write(string)
Vous n'avez pas besoin de définir le codage du tampon sys.stdout.encoding, mais cela vous aide lorsque vous utilisez cette méthode pour tester/comparer la sortie du script.
La question ici (l'exemple montrant comment rediriger la sortie, pas la partie tee
) utilise os.dup2
pour rediriger un flux au niveau du système d'exploitation. C'est bien, car cela s'appliquera également aux commandes que vous créez à partir de votre programme.
J'aime la solution contextmanager, mais si vous avez besoin du tampon stocké avec le fichier ouvert et le support fileno, vous pouvez faire quelque chose comme ceci.
import six
from six.moves import StringIO
class FileWriteStore(object):
def __init__(self, file_):
self.__file__ = file_
self.__buff__ = StringIO()
def __getattribute__(self, name):
if name in {
"write", "writelines", "get_file_value", "__file__",
"__buff__"}:
return super(FileWriteStore, self).__getattribute__(name)
return self.__file__.__getattribute__(name)
def write(self, text):
if isinstance(text, six.string_types):
try:
self.__buff__.write(text)
except:
pass
self.__file__.write(text)
def writelines(self, lines):
try:
self.__buff__.writelines(lines)
except:
pass
self.__file__.writelines(lines)
def get_file_value(self):
return self.__buff__.getvalue()
utilisation
import sys
sys.stdout = FileWriteStore(sys.stdout)
print "test"
buffer = sys.stdout.get_file_value()
# you don't want to print the buffer while still storing
# else it will double in size every print
sys.stdout = sys.stdout.__file__
print buffer
Je pense que vous devriez regarder ces quatre objets:
from test.test_support import captured_stdout, captured_output, \
captured_stderr, captured_stdin
Exemple:
from writer import write
with captured_stdout() as stdout:
write()
print stdout.getvalue().upper()
UPD: Comme Eric l'a dit dans un commentaire, il ne faut pas les utiliser directement, alors je l'ai copié et collé.
# Code from test.test_support:
import contextlib
import sys
@contextlib.contextmanager
def captured_output(stream_name):
"""Return a context manager used by captured_stdout and captured_stdin
that temporarily replaces the sys stream *stream_name* with a StringIO."""
import StringIO
orig_stdout = getattr(sys, stream_name)
setattr(sys, stream_name, StringIO.StringIO())
try:
yield getattr(sys, stream_name)
finally:
setattr(sys, stream_name, orig_stdout)
def captured_stdout():
"""Capture the output of sys.stdout:
with captured_stdout() as s:
print "hello"
self.assertEqual(s.getvalue(), "hello")
"""
return captured_output("stdout")
def captured_stderr():
return captured_output("stderr")
def captured_stdin():
return captured_output("stdin")