¿Cómo manejar bien `with open(…)` y `sys.stdout`?

6 minutos de lectura

avatar de usuario
Jakub M.

A menudo necesito enviar datos a un archivo o, si no se especifica el archivo, a la salida estándar. Yo uso el siguiente fragmento:

if target:
    with open(target, 'w') as h:
        h.write(content)
else:
    sys.stdout.write(content)

Me gustaría reescribirlo y manejar ambos objetivos de manera uniforme.

En el caso ideal sería:

with open(target, 'w') as h:
    h.write(content)

pero esto no funcionará bien porque sys.stdout se cerrará al salir with bloquear y no quiero eso. yo tampoco quiero

stdout = open(target, 'w')
...

porque necesitaría recordar restaurar la salida estándar original.

Relacionado:

  • ¿Redirigir stdout a un archivo en Python?
  • Manejo de excepciones – artículo interesante sobre el manejo de excepciones en Python, en comparación con C++

Editar

Sé que puedo envolver targetdefina una función separada o use administrador de contexto. Busco una solución simple, elegante e idiomática que no requiera más de 5 líneas

  • Lástima que no hayas agregado la edición antes;) De todos modos… alternativamente, simplemente no puedes molestarte en limpiar tu archivo abierto: P

    – lobo

    11/07/2013 a las 20:37

  • Tu primer fragmento de código me parece bien: expresa intención y hace lo que quieres.

    –Max Heiber

    22 oct 2021 a las 17:10


avatar de usuario
Lobo

Solo pensando fuera de la caja aquí, ¿qué tal una costumbre open() ¿método?

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename=None):
    if filename and filename != '-':
        fh = open(filename, 'w')
    else:
        fh = sys.stdout

    try:
        yield fh
    finally:
        if fh is not sys.stdout:
            fh.close()

Úsalo así:

# For Python 2 you need this line
from __future__ import print_function

# writes to some_file
with smart_open('some_file') as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open() as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open('-') as fh:
    print('some output', file=fh)

avatar de usuario
Licuadora

Sigue con tu código actual. Es simple y se puede decir exactamente lo que está haciendo con solo mirarlo.

Otra forma sería con una línea if:

handle = open(target, 'w') if target else sys.stdout
handle.write(content)

if handle is not sys.stdout:
    handle.close()

Pero eso no es mucho más corto que lo que tienes y podría decirse que se ve peor.

También podrías hacer sys.stdout no se puede cerrar, pero eso no parece demasiado Pythonic:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)

  • Puede mantener la imposibilidad de cerrar todo el tiempo que lo necesite creando también un administrador de contexto: with unclosable(sys.stdout): ... configurando sys.stdout.close = lambda: None dentro de este administrador de contexto y luego restablecerlo al valor anterior. Pero esto parece un poco exagerado…

    – glglgl

    11 de julio de 2013 a las 21:14

  • ¡Estoy dividido entre votar “déjalo, puedes decir exactamente lo que está haciendo” y rechazar la horrenda sugerencia que no se puede cerrar!

    – GreenAsJade

    22 de julio de 2016 a las 5:46

  • @GreenAsJade No creo que lo fuera sugerencia haciendo sys.stdout no se puede cerrar, solo notando que se podría hacer. Es mejor mostrar las malas ideas y explicar por qué son malas que no mencionarlas y esperar que los demás no se tropiecen con ellas.

    – cjs

    3 de septiembre de 2020 a las 3:45

avatar de usuario
2rs2ts

¿Por qué LBYL cuando puedes EAFP?

try:
    with open(target, 'w') as h:
        h.write(content)
except TypeError:
    sys.stdout.write(content)

¿Por qué reescribirlo para usar el with/as bloquee uniformemente cuando tiene que hacer que funcione de forma enrevesada? agregarás más líneas y reducir el rendimiento.

  • Excepciones no debe utilizarse para controlar el flujo “normal” de la rutina. ¿Actuación? ¿La aparición de un error será más rápida que if/else?

    – Jakub M.

    11/07/2013 a las 20:36


  • Depende de la probabilidad de que uses uno u otro.

    – 2rs2ts

    11/07/2013 a las 20:37

  • @JakubM. Las excepciones pueden, deben y se usan así en Python.

    – Gareth Latty

    11/07/2013 a las 20:51

  • Teniendo en cuenta que Python for loop sale al detectar un error de StopIteration lanzado por el iterador que está recorriendo, diría que usar excepciones para el control de flujo es completamente Pythonic.

    -Kirk Strauser

    11 de julio de 2013 a las 22:01

  • Asumiendo que target es None cuando se pretende sys.stdout, debe capturar TypeError más bien que IOError.

    – torek

    12 de julio de 2013 a las 1:17


avatar de usuario
Evpok

Una mejora de la respuesta de Wolfh.

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename: str, mode: str="r", *args, **kwargs):
    '''Open files and i/o streams transparently.'''
    if filename == '-':
        if 'r' in mode:
            stream = sys.stdin
        else:
            stream = sys.stdout
        if 'b' in mode:
            fh = stream.buffer  # type: IO
        else:
            fh = stream
        close = False
    else:
        fh = open(filename, mode, *args, **kwargs)
        close = True

    try:
        yield fh
    finally:
        if close:
            try:
                fh.close()
            except AttributeError:
                pass

Esto permite E/S binaria y pasa eventuales argumentos extraños a open si filename es de hecho un nombre de archivo.

Otra posible solución: no intente evitar el método de salida del administrador de contexto, simplemente duplique la salida estándar.

with (os.fdopen(os.dup(sys.stdout.fileno()), 'w')
      if target == '-'
      else open(target, 'w')) as f:
      f.write("Foo")

avatar de usuario
Tommi Komulainen

También elegiría una función contenedora simple, que puede ser bastante simple si puede ignorar el modo (y, en consecuencia, stdin vs. stdout), por ejemplo:

from contextlib import contextmanager
import sys

@contextmanager
def open_or_stdout(filename):
    if filename != '-':
        with open(filename, 'w') as f:
            yield f
    else:
        yield sys.stdout

avatar de usuario
tdelaney

Bien, si nos estamos metiendo en guerras de una sola línea, esto es lo siguiente:

(target and open(target, 'w') or sys.stdout).write(content)

Me gusta el ejemplo original de Jacob siempre que el contexto solo esté escrito en un lugar. Sería un problema si terminas reabriendo el archivo para muchas escrituras. Creo que simplemente tomaría la decisión una vez en la parte superior del script y dejaría que el sistema cierre el archivo al salir:

output = target and open(target, 'w') or sys.stdout
...
output.write('thing one\n')
...
output.write('thing two\n')

Podría incluir su propio controlador de salida si cree que es más ordenado

import atexit

def cleanup_output():
    global output
    if output is not sys.stdout:
        output.close()

atexit(cleanup_output)

  • No creo que su línea única cierre el objeto de archivo. ¿Me equivoco?

    – 2rs2ts

    11/07/2013 a las 22:00

  • @ 2rs2ts – Lo hace… condicionalmente. El refcount del objeto de archivo llega a cero porque no hay variables que lo apunten, por lo que está disponible para que se llame a su método __del__ inmediatamente (en cpython) o más tarde cuando ocurra la recolección de elementos no utilizados. Hay advertencias en el documento para no confiar en que esto siempre funcionará, pero lo uso todo el tiempo en scripts más cortos. Algo grande que dura mucho tiempo y abre muchos archivos… bueno, supongo que usaría ‘con’ o ‘intentar/finalmente’.

    – tdelaney

    11 de julio de 2013 a las 22:07


  • TIL. No sabía que los objetos de archivo’ __del__ haría eso

    – 2rs2ts

    11 de julio de 2013 a las 22:10

  • @ 2rs2ts: CPython usa un recolector de basura de conteo de referencias (con un GC “real” debajo invocado según sea necesario) para que pueda cerrar el archivo tan pronto como elimine todas las referencias al controlador de flujo. Jython y aparentemente IronPython solo tienen el GC “real”, por lo que no cierran el archivo hasta un eventual GC.

    – torek

    12 de julio de 2013 a las 1:40

¿Ha sido útil esta solución?