Désactiver la mise en mémoire tampon de sortie

532

La mise en mémoire tampon de sortie est-elle activée par défaut dans l'interpréteur Python pour sys.stdout ?

Si la réponse est positive, quelles sont toutes les façons de la désactiver?

Suggestions jusqu'à présent:

  1. Utilisez le -ucommutateur de ligne de commande
  2. Enveloppez sys.stdoutdans un objet qui se vide après chaque écriture
  3. Définir PYTHONUNBUFFEREDenv var
  4. sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

Existe-t-il un autre moyen de définir un indicateur global dans sys/ par sys.stdoutprogramme pendant l'exécution?

Eli Bendersky
la source
7
Pour «imprimer» en Python 3, consultez cette réponse .
Antti Haapala
1
Je pense qu'un inconvénient -uest qu'il ne fonctionnera pas pour le bytecode compilé ou pour les applications avec un __main__.pyfichier comme point d'entrée.
akhan
La logique d'initialisation complète de CPython est ici: github.com/python/cpython/blob/v3.8.2/Python/…
Beni Cherniavsky-Paskin

Réponses:

443

De Magnus Lycka répondre sur une liste de diffusion :

Vous pouvez ignorer la mise en mémoire tampon pour un processus python entier en utilisant "python -u" (ou #! / Usr / bin / env python -u etc.) ou en définissant la variable d'environnement PYTHONUNBUFFERED.

Vous pouvez également remplacer sys.stdout par un autre flux comme le wrapper qui effectue un vidage après chaque appel.

class Unbuffered(object):
   def __init__(self, stream):
       self.stream = stream
   def write(self, data):
       self.stream.write(data)
       self.stream.flush()
   def writelines(self, datas):
       self.stream.writelines(datas)
       self.stream.flush()
   def __getattr__(self, attr):
       return getattr(self.stream, attr)

import sys
sys.stdout = Unbuffered(sys.stdout)
print 'Hello'
Seb
la source
71
Le sys.stdout d'origine est toujours disponible en tant que sys .__ stdout__. Juste au cas où vous en auriez besoin =)
Antti Rasinen
40
#!/usr/bin/env python -une fonctionne pas !! voir ici
wim
6
__getattr__juste pour éviter l'héritage?!
Vladimir Keleshev
33
Quelques notes pour éviter certains maux de tête: Comme je l'ai remarqué, la mise en mémoire tampon de sortie fonctionne différemment selon que la sortie va vers un tty ou un autre processus / pipe. S'il va à un tty, il est vidé après chaque \ n , mais dans un canal, il est mis en mémoire tampon. Dans ce dernier cas, vous pouvez utiliser ces solutions de rinçage. En Cpython (pas dans Pypy !!!): Si vous parcourez l'entrée avec la ligne for dans sys.stdin: ... alors la boucle for collectera un certain nombre de lignes avant que le corps de la boucle ne soit exécuté. Cela se comportera comme une mise en mémoire tampon, bien que ce soit plutôt par lots. Au lieu de cela, faites en vrai: line = sys.stdin.readline ()
tzp
5
@tzp: vous pouvez utiliser au iter()lieu de la whileboucle: for line in iter(pipe.readline, ''):. Vous n'en avez pas besoin sur Python 3 où les for line in pipe:rendements le plus tôt possible.
jfs
122

Je préfère mettre ma réponse dans Comment vider la sortie de la fonction d'impression? ou dans la fonction d'impression de Python qui vide le tampon quand il est appelé? , mais comme ils ont été marqués comme des doublons de celui-ci (ce que je ne suis pas d'accord), je vais y répondre ici.

Depuis Python 3.3, print () supporte l'argument de mot-clé "flush" ( voir documentation ):

print('Hello World!', flush=True)
Cristóvão D. Sousa
la source
77
# reopen stdout file descriptor with write mode
# and 0 as the buffer size (unbuffered)
import io, os, sys
try:
    # Python 3, open as binary, then wrap in a TextIOWrapper with write-through.
    sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0), write_through=True)
    # If flushing on newlines is sufficient, as of 3.7 you can instead just call:
    # sys.stdout.reconfigure(line_buffering=True)
except TypeError:
    # Python 2
    sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

Crédits: "Sebastian", quelque part sur la liste de diffusion Python.

Federico A. Ramponi
la source
En Python3, vous pouvez simplement remplacer le nom de la fonction d'impression par une fonction de rinçage. C'est un sale tour cependant!
meawoppl
16
@meawoppl: vous pouvez passer un flush=Trueparamètre pour print()fonctionner depuis Python 3.3.
jfs
La modification de la réponse pour afficher la réponse n'est pas valide dans la version récente de python
Mike
à la fois os.fdopen(sys.stdout.fileno(), 'wb', 0)(notez le bpour binaire) et flush=Truetravaillez pour moi en 3.6.4. Cependant, si vous utilisez un sous-processus pour démarrer un autre script, assurez-vous que vous avez spécifié python3, si vous avez plusieurs instances de python installées.
not2qubit
1
@ not2qubit: si vous utilisez, os.fdopen(sys.stdout.fileno(), 'wb', 0)vous vous retrouvez avec un objet fichier binaire, pas un TextIOflux. Vous devez ajouter un TextIOWrapperau mélange (en veillant à activer write_throughpour éliminer tous les tampons, ou à utiliser line_buffering=Truepour vider uniquement sur les nouvelles lignes).
Martijn Pieters
55

Oui, ça l'est.

Vous pouvez le désactiver sur la ligne de commande avec le commutateur "-u".

Alternativement, vous pouvez appeler .flush () sur sys.stdout à chaque écriture (ou l'envelopper avec un objet qui le fait automatiquement)

Brian
la source
19

Cela concerne la réponse de Cristóvão D. Sousa, mais je ne pourrais pas encore commenter.

Une manière simple d'utiliser l' flushargument mot - clé de Python 3 afin d' avoir toujours une sortie sans tampon est:

import functools
print = functools.partial(print, flush=True)

par la suite, l'impression videra toujours directement la sortie (sauf indication contraire flush=False).

Notez, (a) que cela ne répond que partiellement à la question car il ne redirige pas toute la sortie. Mais je suppose que printc'est la façon la plus courante de créer une sortie vers stdout/ stderren python, donc ces 2 lignes couvrent probablement la plupart des cas d'utilisation.

Notez (b) qu'il ne fonctionne que dans le module / script où vous l'avez défini. Cela peut être utile lors de l'écriture d'un module car il ne dérange pas avec le sys.stdout.

Python 2 ne fournit pas l' flushargument, mais vous pouvez émuler une printfonction de type Python 3 comme décrit ici https://stackoverflow.com/a/27991478/3734258 .

Tim
la source
1
Sauf qu'il n'y a pas de flushkwarg en python2.
o11c
@ o11c, oui vous avez raison. J'étais sûr de l'avoir testé mais d'une certaine manière, j'étais apparemment confus (: j'ai modifié ma réponse, j'espère que ça va maintenant. Merci!
Tim
14
def disable_stdout_buffering():
    # Appending to gc.garbage is a way to stop an object from being
    # destroyed.  If the old sys.stdout is ever collected, it will
    # close() stdout, which is not good.
    gc.garbage.append(sys.stdout)
    sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

# Then this will give output in the correct order:
disable_stdout_buffering()
print "hello"
subprocess.call(["echo", "bye"])

Sans enregistrer l'ancien sys.stdout, disable_stdout_buffering () n'est pas idempotent, et plusieurs appels entraîneront une erreur comme celle-ci:

Traceback (most recent call last):
  File "test/buffering.py", line 17, in <module>
    print "hello"
IOError: [Errno 9] Bad file descriptor
close failed: [Errno 9] Bad file descriptor

Une autre possibilité est:

def disable_stdout_buffering():
    fileno = sys.stdout.fileno()
    temp_fd = os.dup(fileno)
    sys.stdout.close()
    os.dup2(temp_fd, fileno)
    os.close(temp_fd)
    sys.stdout = os.fdopen(fileno, "w", 0)

(L'ajout à gc.garbage n'est pas une si bonne idée car c'est là que les cycles non exploitables sont mis en place, et vous voudrez peut-être les vérifier.)

Mark Seaborn
la source
2
Si l'ancien stdoutvit toujours sys.__stdout__comme certains l'ont suggéré, la poubelle ne sera pas nécessaire, non? C'est un truc sympa cependant.
Thomas Ahle
1
Comme avec la réponse de @ Federico, cela ne fonctionnera pas avec Python 3, car cela lèvera l'exception ValueError: can't have unbuffered text I/Olors de l'appel print().
gbmhunter
Votre "autre possibilité" semble au premier abord comme la solution la plus robuste, mais malheureusement elle souffre d'une condition de concurrence dans le cas où un autre thread appelle open () après votre sys.stdout.close () et avant votre os.dup2 (temp_fd, fileno ). J'ai découvert cela lorsque j'ai essayé d'utiliser votre technique sous ThreadSanitizer, qui fait exactement cela. L'échec est d'autant plus fort que dup2 () échoue avec EBUSY quand il court avec open () comme ça; voir stackoverflow.com/questions/23440216/…
Don Hatch
13

Les travaux suivants dans Python 2.6, 2.7 et 3.2:

import os
import sys
buf_arg = 0
if sys.version_info[0] == 3:
    os.environ['PYTHONUNBUFFERED'] = '1'
    buf_arg = 1
sys.stdout = os.fdopen(sys.stdout.fileno(), 'a+', buf_arg)
sys.stderr = os.fdopen(sys.stderr.fileno(), 'a+', buf_arg)
Gummbum
la source
Exécutez ça deux fois et ça plante sur Windows :-)
Michael Clerx
@MichaelClerx Mmm hmm, n'oubliez pas de fermer vos fichiers xD.
Python 3.5 sur Raspbian 9 me donne OSError: [Errno 29] Illegal seekla lignesys.stdout = os.fdopen(sys.stdout.fileno(), 'a+', buf_arg)
sdbbs
12

Oui, il est activé par défaut. Vous pouvez le désactiver en utilisant l'option -u sur la ligne de commande lors de l'appel de python.

Nathan
la source
7

Vous pouvez également exécuter Python avec l' utilitaire stdbuf :

stdbuf -oL python <script>

Dyomas
la source
2
La mise en mémoire tampon de ligne (comme -oLactivé) est toujours en mémoire tampon - voir f / e stackoverflow.com/questions/58416853/… , demandant pourquoi la end=''sortie n'est plus affichée immédiatement.
Charles Duffy
Vrai, mais la mise en mémoire tampon de ligne est la valeur par défaut (avec un tty), est-il donc logique d'écrire du code en supposant que la sortie est totalement sans tampon - peut-être mieux de explicitement print(..., end='', flush=True)où cela est improtant? OTOH, lorsque plusieurs programmes écrivent simultanément sur la même sortie, le compromis a tendance à passer de la progression immédiate à la réduction des mélanges de sortie, et la mise en mémoire tampon des lignes devient attrayante. Alors peut - être qu'il est préférable de ne pas écrire explicitement flushet le contrôle tampon externe?
Beni Cherniavsky-Paskin
Je pense que non. Le processus lui-même devrait décider quand et pourquoi il appelle flush. Le contrôle de la mise en mémoire tampon externe est une solution de contournement
obligatoire
7

En Python 3, vous pouvez patcher la fonction d'impression pour toujours envoyer flush = True:

_orig_print = print

def print(*args, **kwargs):
    _orig_print(*args, flush=True, **kwargs)

Comme indiqué dans un commentaire, vous pouvez simplifier cela en liant le paramètre flush à une valeur, via functools.partial:

print = functools.partial(print, flush=True)
Oliver
la source
3
Je me demandais, mais ne serait-ce pas un cas d'utilisation parfait pour functools.partial ?
0xC0000022L
Merci @ 0xC0000022L, cela donne une meilleure apparence! print = functools.partial(print, flush=True)fonctionne bien pour moi.
MarSoft
@ 0xC0000022L en effet, j'ai mis à jour le message pour montrer cette option, merci de l'avoir signalé
Oliver
3
Si vous voulez que cela s'applique partout, import builtins; builtins.print = partial(print, flush=True)
Perkins
4

Vous pouvez également utiliser fcntl pour modifier les indicateurs de fichier à la volée.

fl = fcntl.fcntl(fd.fileno(), fcntl.F_GETFL)
fl |= os.O_SYNC # or os.O_DSYNC (if you don't care the file timestamp updates)
fcntl.fcntl(fd.fileno(), fcntl.F_SETFL, fl)
Jimx
la source
1
Il y a un équivalent Windows: stackoverflow.com/questions/881696/…
Tobu
12
O_SYNC n'a rien à voir avec la mise en mémoire tampon au niveau de l'espace utilisateur que cette question pose.
apenwarr
4

Il est possible de remplacer uniquement la write méthode de sys.stdoutcelle qui appelle flush. L'implémentation de méthode suggérée est ci-dessous.

def write_flush(args, w=stdout.write):
    w(args)
    stdout.flush()

La valeur par défaut de l' wargument conservera la writeréférence de méthode d' origine . Une fois write_flush défini, l'original writepeut être remplacé.

stdout.write = write_flush

Le code suppose qu'il stdoutest importé de cette façon from sys import stdout.

Vasily E.
la source
3

Vous pouvez créer un fichier sans tampon et affecter ce fichier à sys.stdout.

import sys 
myFile= open( "a.log", "w", 0 ) 
sys.stdout= myFile

Vous ne pouvez pas modifier comme par magie la sortie standard fournie par le système; car il est fourni à votre programme python par le système d'exploitation.

S.Lott
la source
3

Variante qui fonctionne sans planter (au moins sur win32; python 2.7, ipython 0.12) puis appelée par la suite (plusieurs fois):

def DisOutBuffering():
    if sys.stdout.name == '<stdout>':
        sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

    if sys.stderr.name == '<stderr>':
        sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
Laimis
la source
Êtes-vous sûr que ce n'est pas mis en mémoire tampon?
quantum
1
Devriez-vous vérifier au sys.stdout is sys.__stdout__lieu de compter sur l'objet de remplacement ayant un attribut de nom?
leewz
cela fonctionne très bien si le gunicorn ne respecte pas PYTHONUNBUFFERED pour une raison quelconque.
Brian Arsuaga
3

(J'ai posté un commentaire, mais il s'est perdu d'une manière ou d'une autre. Donc, encore une fois :)

  1. Comme je l'ai remarqué, CPython (au moins sous Linux) se comporte différemment selon où va la sortie. S'il va à un tty, la sortie est purgée après chaque ' \n'
    S'il va à un tuyau / processus, alors il est tamponné et vous pouvez utiliser les flush()solutions basées ou l' option -u recommandée ci-dessus.

  2. Légèrement lié à la mise en mémoire tampon de sortie:
    si vous parcourez les lignes en entrée avec

    for line in sys.stdin:
    ...

puis pour la mise en œuvre en CPython recueillera l'entrée pendant un certain temps et puis exécutez le corps de la boucle pour une série de lignes d'entrée. Si votre script est sur le point d'écrire la sortie pour chaque ligne d'entrée, cela peut ressembler à une mise en mémoire tampon de sortie, mais il s'agit en fait d'un traitement par lots, et par conséquent, aucune des flush()techniques, etc. n'aidera cela. Fait intéressant, vous n'avez pas ce comportement dans Pypy . Pour éviter cela, vous pouvez utiliser

while True: line=sys.stdin.readline()
...

tzp
la source
voici votre commentaire . Il peut s'agir d'un bug sur les anciennes versions de Python. Pourriez-vous fournir un exemple de code? Quelque chose comme for line in sys.stdinvsfor line in iter(sys.stdin.readline, "")
jfs
pour la ligne dans sys.stdin: print ("Line:" + line); sys.stdout.flush ()
tzp
il ressemble au bug de lecture anticipée . Cela ne devrait arriver que sur Python 2 et si stdin est un pipe. Le code dans mon commentaire précédent démontre le problème ( for line in sys.stdinfournit une réponse retardée)
jfs
2

Une façon d'obtenir une sortie non tamponnée serait d'utiliser à la sys.stderrplace de sys.stdoutou d'appeler simplement sys.stdout.flush()pour forcer explicitement l'écriture à se produire.

Vous pouvez facilement rediriger tout ce qui est imprimé en faisant:

import sys; sys.stdout = sys.stderr
print "Hello World!"

Ou pour rediriger juste pour une printinstruction particulière :

print >>sys.stderr, "Hello World!"

Pour réinitialiser stdout, vous pouvez simplement faire:

sys.stdout = sys.__stdout__
stderr
la source
1
Cela peut devenir très déroutant lorsque vous essayez ensuite de capturer la sortie à l'aide de la redirection standard et que vous ne capturez rien! ps votre stdout est en gras et tout ça.
espace libre du
1
Une grande mise en garde concernant l'impression sélective sur stderr est que cela fait que les lignes semblent déplacées, donc à moins que vous n'ayez également un horodatage, cela pourrait devenir très déroutant.
haridsv