Escriba Python stdout para presentar inmediatamente

51

Al intentar escribir la salida estándar de un script de Python en un archivo de texto ( python script.py > log), el archivo de texto se crea cuando se inicia el comando, pero el contenido real no se escribe hasta que finaliza el script de Python. Por ejemplo:

script.py:

import time
for i in range(10):
    print('bla')
    time.sleep(5)

se imprime en stdout cada 5 segundos cuando se llama con python script.py, pero cuando llamo python script.py > log, el tamaño del archivo de registro permanece cero hasta que finaliza el script. ¿Es posible escribir directamente en el archivo de registro, de modo que pueda seguir el progreso de la secuencia de comandos (por ejemplo, usando tail)?

EDITAR Resulta que python -u script.pyhace el truco, no sabía sobre el almacenamiento en búfer de stdout.

Bart
fuente
1
@jezmck, podría haber entendido mal la pregunta.
zyxue

Respuestas:

64

Esto sucede porque normalmente cuando el proceso STDOUT se redirige a algo diferente a un terminal, la salida se almacena en un búfer de tamaño específico del sistema operativo (quizás 4k u 8k en muchos casos). Por el contrario, cuando se envía a un terminal, STDOUT tendrá una línea de búfer o nada de búfer, por lo que verá la salida después de cada uno \no para cada carácter.

En general, puede cambiar el almacenamiento intermedio STDOUT con la stdbufutilidad:

stdbuf -oL python script.py > log

Ahora, si es así tail -F log, debería ver cada salida de línea inmediatamente a medida que se genera.


Alternativamente, el enjuague explícito del flujo de salida después de cada impresión debería lograr lo mismo. Parece que sys.stdout.flush()debería lograr esto en Python. Si está utilizando Python 3.3 o posterior, la printfunción también tiene una flushpalabra clave que hace esto: print('hello', flush=True).

Trauma digital
fuente
8
¡Gracias, no sabía sobre el almacenamiento en búfer! Sabiendo eso, Google rápidamente me dijo que eso python -u script.pyfunciona. EDITAR Tantas respuestas a la vez, acepté la suya, ya que me señaló en la dirección del almacenamiento en búfer.
Bart
1
@julbra Cool, sí, tampoco sabía que Python tenía esa opción. Algunos programas de línea de comandos también tienen opciones similares, por ejemplo , --line-bufferedpara grep, pero otros no. stdbufes la utilidad general de catchall para tratar con aquellos que no lo hacen.
Trauma digital
@DigitalTrauma: ¿No es mejor no usar ningún búfer, es decir, stdbuf -o0 python script.py > logen este tipo de circunstancias determinadas?
heemayl
@heemayl -oLes un compromiso. En general, los buffers más grandes proporcionarán un mejor rendimiento al redirigir a algún lugar (menos llamadas al sistema y menos operaciones de E / S). Sin embargo, si es absolutamente necesario ver cada carácter a medida que sale, entonces sí, -o0sería necesario.
Trauma digital
@Paul Evite copiar y pegar contenidos entre las respuestas, o al menos mencione los autores originales que proporcionaron el contenido.
Bakuriu
44

Esto debería hacer el trabajo:

import time, sys
for i in range(10):
    print('bla')
    sys.stdout.flush()
    time.sleep(5)

Como Python almacenará el búfer stdoutde forma predeterminada, aquí he utilizado sys.stdout.flush()para vaciar el búfer.

Otra solución sería utilizar el -uinterruptor (sin búfer) de python. Entonces, lo siguiente también lo hará:

python -u script.py >> log
heemayl
fuente
11

La variación sobre el tema del uso de la propia opción de python para la salida sin búfer sería usar #!/usr/bin/python -ucomo primera línea.

Con #!/usr/bin/env pythonese argumento extra no funcionará, así que, alternativamente, uno podría ejecutarlo PYTHONUNBUFFERED=1 ./my_scriipt.py > output.txto hacerlo en dos pasos:

$ export PYTHONUNBUFFERED=1
$ ./myscript.py
Sergiy Kolodyazhnyy
fuente
10

Deberías pasar flush=Truea la printfunción:

import time

for i in range(10):
    print('bla', flush=True)
    time.sleep(5)

Según la documentación, de forma predeterminada, printno se aplica nada sobre el vaciado:

Si el resultado se almacena en búfer generalmente se determina por archivo, pero si el flushargumento de la palabra clave es verdadero, el flujo se vacía a la fuerza.

Y la documentación de syslos strems dice:

Cuando son interactivas, las transmisiones estándar tienen un buffer de línea. De lo contrario, se almacenan en bloques como archivos de texto normales. Puede anular este valor con la -uopción de línea de comandos.


Si está atrapado con una versión antigua de python, debe llamar al flushmétodo de la sys.stdouttransmisión:

import sys
import time

for i in range(10):
    print('bla')
    sys.stdout.flush()
    time.sleep(5)
Bakuriu
fuente
1
El argumento flush = True funciona bien con Python 3.4.2, de hecho no funciona con el antiguo (..) Python 2.7.9
Bart
Esta respuesta sugiere lo mismo que DigitalTraumadijo 10 horas antes. Deberías votar su publicación, no publicar la misma cosa de nuevo.
dotancohen
44
@dotancohen En realidad, la parte sobre print(flush=True)fue agregada a esa respuesta después de la mía por un autor externo . Me parece de mal gusto extraer contenidos de mi respuesta para ponerlos en otro sin crédito. Decidí agregar mi respuesta únicamente porque ninguna respuesta mencionaba la forma más simple de lograr lo que el OP quería en las versiones más recientes de python, y agregué la "forma antigua" solo para completar. La próxima vez, verifique el historial de revisiones antes de comentar o votar a favor.
Bakuriu
@Bakuriu: ¡Lo siento entonces! Esto muestra una buena razón para publicar siempre por qué al hacer downvoting . ¿Podrías editar un poco la publicación para que pueda cambiar mi voto a favor? ¡Gracias!
dotancohen
Se debe trabajar con Python 2.7 si lo hace __future__la importación: from __future__ import print_function. Pero sí, eso es solo por compatibilidad con Python 3
Sergiy Kolodyazhnyy