Deshabilitar el búfer de salida

532

¿Está habilitado el almacenamiento en búfer de salida de forma predeterminada en el intérprete de Python sys.stdout?

Si la respuesta es positiva, ¿cuáles son todas las formas de desactivarla?

Sugerencias hasta el momento:

  1. Use el -uinterruptor de línea de comando
  2. Envuelva sys.stdouten un objeto que se enjuague después de cada escritura
  3. Establecer PYTHONUNBUFFEREDenv var
  4. sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

¿Hay alguna otra forma de establecer algún indicador global sys/ sys.stdoutprogramáticamente durante la ejecución?

Eli Bendersky
fuente
77
Para `imprimir 'en Python 3, vea esta respuesta .
Antti Haapala
1
Creo que un inconveniente -ues que no funcionará para el bytecode compilado o para aplicaciones con un __main__.pyarchivo como punto de entrada.
akhan
La lógica completa de inicialización de CPython está aquí: github.com/python/cpython/blob/v3.8.2/Python/…
Beni Cherniavsky-Paskin

Respuestas:

443

De la respuesta de Magnus Lycka en una lista de correo :

Puede omitir el almacenamiento en búfer para un proceso completo de python usando "python -u" (o #! / Usr / bin / env python -u, etc.) o configurando la variable de entorno PYTHONUNBUFFERED.

También puede reemplazar sys.stdout con alguna otra secuencia como wrapper que realiza una descarga después de cada llamada.

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
fuente
71
El sys.stdout original todavía está disponible como sys .__ stdout__. Por si acaso lo necesitas =)
Antti Rasinen
40
#!/usr/bin/env python -uno funciona !! mira aquí
wim
66
__getattr__solo para evitar la herencia?
Vladimir Keleshev
33
Algunas notas para ahorrar algunos dolores de cabeza: como noté, el almacenamiento en búfer de salida funciona de manera diferente dependiendo de si la salida va a un tty u otro proceso / tubería. Si va a un tty, entonces se vacía después de cada \ n , pero en una tubería se almacena en búfer. En este último caso, puede hacer uso de estas soluciones de lavado. En Cpython (¡no en pypy!): Si itera sobre la entrada con for en sys.stdin: ... entonces el bucle for recopilará varias líneas antes de ejecutar el cuerpo del bucle. Esto se comportará como almacenamiento en búfer, aunque es bastante discontinuo. En cambio, hazlo mientras sea verdadero: line = sys.stdin.readline ()
tzp
55
@tzp: se puede utilizar iter()en lugar del whilebucle: for line in iter(pipe.readline, ''):. No lo necesita en Python 3, donde for line in pipe:rinde lo antes posible.
jfs
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éditos: "Sebastian", en algún lugar de la lista de correo de Python.

Federico A. Ramponi
fuente
En Python3, puede anular el nombre de la función de impresión con una función de vaciado. ¡Pero es un truco sucio!
meawoppl
16
@meawoppl: podría pasar el flush=Trueparámetro a la print()función desde Python 3.3.
jfs
La edición de la respuesta para mostrar la respuesta no es válida en la versión reciente de python
Mike
ambos os.fdopen(sys.stdout.fileno(), 'wb', 0)(tenga en cuenta el bpara binario) y flush=Truetrabajen para mí en 3.6.4. Sin embargo, si está utilizando un subproceso para iniciar otro script, asegúrese de haber especificado python3si tiene instancias múltiples de python instaladas.
not2qubit
1
@ not2qubit: si usas os.fdopen(sys.stdout.fileno(), 'wb', 0)terminas con un objeto de archivo binario, no una TextIOsecuencia. Tendría que agregar un TextIOWrappera la mezcla (asegurándose de habilitar write_throughpara eliminar todos los búferes, o usar line_buffering=Truesolo para limpiar las nuevas líneas).
Martijn Pieters
55

Sí lo es.

Puede deshabilitarlo en la línea de comandos con el interruptor "-u".

Alternativamente, puede llamar a .flush () en sys.stdout en cada escritura (o envolverlo con un objeto que lo haga automáticamente)

Brian
fuente
19

Esto se relaciona con la respuesta de Cristóvão D. Sousa, pero aún no puedo comentar.

Una forma directa de utilizar el flushargumento de palabra clave de Python 3 para tener siempre una salida sin búfer es:

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

luego, la impresión siempre vaciará la salida directamente (excepto que flush=Falsese indique).

Tenga en cuenta, (a) que esto responde la pregunta solo parcialmente, ya que no redirige toda la salida. Pero supongo que printes la forma más común de crear resultados en stdout/ stderren python, por lo que estas 2 líneas cubren probablemente la mayoría de los casos de uso.

Tenga en cuenta (b) que solo funciona en el módulo / script donde lo definió. Esto puede ser bueno al escribir un módulo, ya que no se mete con el sys.stdout.

Python 2 no proporciona el flushargumento, pero podría emular una printfunción de tipo Python 3 como se describe aquí https://stackoverflow.com/a/27991478/3734258 .

tim
fuente
1
Excepto que no hay flushkwarg en python2.
o11c
@ o11c, sí, tienes razón. Estaba seguro de que lo probé, pero de alguna manera estaba aparentemente confundido (: modifiqué mi respuesta, espero que esté bien ahora. ¡Gracias!
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"])

Sin guardar el viejo sys.stdout, disable_stdout_buffering () no es idempotente, y múltiples llamadas resultarán en un error como este:

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

Otra posibilidad es:

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)

(Agregar a gc.garbage no es una buena idea porque es donde se ponen los ciclos no liberables, y es posible que desee verificarlos).

Mark Seaborn
fuente
2
Si lo viejo stdoutsigue vivo sys.__stdout__como algunos han sugerido, la basura no será necesaria, ¿verdad? Sin embargo, es un truco genial.
Thomas Ahle
1
Al igual que con la respuesta de @ Federico, esto no funcionará con Python 3, ya que arrojará la excepción ValueError: can't have unbuffered text I/Oal llamar print().
gbmhunter
Al principio, su "otra posibilidad" parece la solución más sólida, pero desafortunadamente sufre una condición de carrera en el caso de que otro hilo llame a open () después de su sys.stdout.close () y antes de su os.dup2 (temp_fd, fileno ) Descubrí esto cuando intenté usar su técnica en ThreadSanitizer, que hace exactamente eso. El fracaso se hace más fuerte por el hecho de que dup2 () falla con EBUSY cuando corre con open () así; ver stackoverflow.com/questions/23440216/…
Don Hatch el
13

Lo siguiente funciona en Python 2.6, 2.7 y 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)
Goma de mascar
fuente
Ejecutar eso dos veces y se bloquea en Windows :-)
Michael Clerx
@MichaelClerx Mmm hmm, recuerda siempre cerrar tus archivos xD.
Python 3.5 en Raspbian 9 me da OSError: [Errno 29] Illegal seekpara la líneasys.stdout = os.fdopen(sys.stdout.fileno(), 'a+', buf_arg)
sdbbs
12

Sí, está habilitado por defecto. Puede deshabilitarlo utilizando la opción -u en la línea de comando cuando llame a python.

Nathan
fuente
7

También puede ejecutar Python con la utilidad stdbuf :

stdbuf -oL python <script>

dyomas
fuente
2
El almacenamiento en línea (como -oLhabilitado) todavía está almacenando en búfer: consulte f / e stackoverflow.com/questions/58416853/… , preguntando por qué la end=''salida ya no se muestra de inmediato.
Charles Duffy
Es cierto, pero el almacenamiento en línea es el predeterminado (con un tty), entonces, ¿tiene sentido escribir código suponiendo que la salida esté totalmente sin búfer print(..., end='', flush=True)? OTOH, cuando varios programas escriben en la misma salida simultáneamente, la compensación tiende a pasar de ver un progreso inmediato a reducir las mezclas de salida, y el almacenamiento en línea se vuelve atractivo. Entonces, ¿tal vez es mejor no escribir explícitamente flushy controlar el almacenamiento en búfer externamente?
Beni Cherniavsky-Paskin
Creo que no. El proceso en sí mismo debe decidir cuándo y por qué llama flush. El control de almacenamiento en búfer externo es una solución
forzada
7

En Python 3, puede parchear la función de impresión para enviar siempre flush = True:

_orig_print = print

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

Como se señaló en un comentario, puede simplificar esto vinculando el parámetro de vaciado a un valor, a través de functools.partial:

print = functools.partial(print, flush=True)
Oliver
fuente
3
Solo me pregunto, pero ¿no sería un caso de uso perfecto functools.partial?
0xC0000022L
Gracias @ 0xC0000022L, ¡esto lo hace ver mejor! print = functools.partial(print, flush=True)funciona bien para mi
MarSoft
@ 0xC0000022L de hecho, he actualizado la publicación para mostrar esa opción, gracias por señalarlo
Oliver
3
Si quieres que se aplican a todas partes,import builtins; builtins.print = partial(print, flush=True)
Perkins
4

También puede usar fcntl para cambiar las marcas de archivo al vuelo.

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
fuente
1
Hay un equivalente de Windows: stackoverflow.com/questions/881696/…
Tobu
12
O_SYNC no tiene nada que ver con el búfer de nivel de espacio de usuario sobre el que se pregunta esta pregunta.
apenwarr
4

Es posible anular solo el write método de sys.stdoutuno que llame flush. La implementación del método sugerido se encuentra a continuación.

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

El valor predeterminado del wargumento mantendrá la writereferencia del método original . Después de que write_flush se define, el original writepodría ser anulado.

stdout.write = write_flush

El código supone que stdoutse importa de esta manera from sys import stdout.

Vasily E.
fuente
3

Puede crear un archivo sin búfer y asignar este archivo a sys.stdout.

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

No puede cambiar mágicamente la salida estándar proporcionada por el sistema; ya que es suministrado a su programa python por el sistema operativo.

S.Lott
fuente
3

Variante que funciona sin fallar (al menos en win32; python 2.7, ipython 0.12) y luego se llama posteriormente (varias veces):

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
fuente
¿Estás seguro de que esto no está protegido?
Quantum
1
¿Debería verificar en sys.stdout is sys.__stdout__lugar de confiar en que el objeto de reemplazo tenga un atributo de nombre?
leewz
esto funciona muy bien si gunicorn no respeta PYTHONUNBUFFERED por alguna razón.
Brian Arsuaga
3

(He publicado un comentario, pero se perdió de alguna manera. Entonces, de nuevo :)

  1. Como noté, CPython (al menos en Linux) se comporta de manera diferente dependiendo de dónde vaya la salida. Si va a un tty, entonces la salida se vacía después de cada ' \n'
    Si va a una tubería / proceso, entonces se almacena en búfer y puede usar las flush()soluciones basadas o la opción -u recomendada anteriormente.

  2. Ligeramente relacionado con el búfer de salida:
    si itera sobre las líneas en la entrada con

    for line in sys.stdin:
    ...

entonces el para aplicación en CPython recogerá la entrada por un tiempo y luego ejecutar el cuerpo del bucle para un grupo de líneas de entrada. Si su script está a punto de escribir la salida para cada línea de entrada, esto podría parecer un buffer de salida, pero en realidad es un proceso por lotes, y por lo tanto, ninguna de las flush()técnicas, etc., lo ayudará. Curiosamente, no tienes este comportamiento en pypy . Para evitar esto, puede usar

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

tzp
fuente
Aquí está tu comentario . Puede ser un error en versiones anteriores de Python. ¿Podría proporcionar un código de ejemplo? Algo así como for line in sys.stdinvs.for line in iter(sys.stdin.readline, "")
jfs
para línea en sys.stdin: print ("Línea:" + línea); sys.stdout.flush ()
tzp
parece el error de lectura anticipada . Solo debería suceder en Python 2 y si stdin es una tubería. El código en mi comentario anterior demuestra el problema ( for line in sys.stdinproporciona una respuesta retrasada)
jfs
2

Una forma de obtener resultados sin búfer sería usar en sys.stderrlugar de sys.stdouto simplemente llamar sys.stdout.flush()para forzar explícitamente a que ocurra una escritura.

Puede redirigir fácilmente todo lo impreso haciendo:

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

O para redirigir solo para una printdeclaración particular :

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

Para restablecer stdout solo puedes hacer:

sys.stdout = sys.__stdout__
stderr
fuente
1
¡Esto puede ser muy confuso cuando luego intentes capturar la salida usando la redirección estándar y descubras que no estás capturando nada! PD: tu stdout está en negrita y esas cosas.
Freespace
1
Una gran precaución sobre la impresión selectiva en stderr es que esto hace que las líneas aparezcan fuera de lugar, por lo que, a menos que también tenga una marca de tiempo, esto podría ser muy confuso.
haridsv