¿Cómo manejar tanto `with open (...)` como `sys.stdout` correctamente?

92

A menudo necesito enviar datos a un archivo o, si el archivo no está especificado, a la salida estándar. Utilizo 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 cierra al salir del withbloque y no quiero eso. Yo tampoco quiero

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

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

Relacionado:

Editar

Sé que puedo ajustar target, definir una función separada o usar el administrador de contexto . Busco una solución sencilla, elegante e idiomática que no requiera más de 5 líneas.

Jakub M.
fuente
Lástima que no haya agregado la edición antes;) De todos modos ... alternativamente, simplemente no puede molestarse en limpiar su archivo abierto: P
Wolph

Respuestas:

93

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

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()

Úselo 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)
Wolph
fuente
28

Cíñete a tu código actual. Es simple y puedes saber 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 de lo que tienes y posiblemente se ve peor.

También podría hacer sys.stdoutque no se pueda 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)
Licuadora
fuente
2
Puede mantener la invisibilidad durante el tiempo que lo necesite creando un administrador de contexto para él también: with unclosable(sys.stdout): ...configurándolo sys.stdout.close = lambda: Nonedentro de este administrador de contexto y restableciéndolo al valor anterior después. Pero esto parece un poco demasiado inverosímil ...
glglgl
3
¡Estoy dividido entre votar por "déjalo, puedes decir exactamente lo que está haciendo" y votar en contra de la horrenda sugerencia que no se puede cerrar!
GreenAsJade
@GreenAsJade No creo que estuviera sugiriendo hacer que no se sys.stdoutpueda cerrar, solo señalando que se podría hacer. Es mejor mostrar malas ideas y explicar por qué son malas que no mencionarlas y esperar que otros no las encuentren.
cjs
8

¿Por qué LBYL cuando puede EAFP?

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

¿Por qué reescribirlo para usar el bloque with/ asuniformemente cuando tienes que hacerlo funcionar de una manera complicada? Agregará más líneas y reducirá el rendimiento.

2rs2ts
fuente
3
No se deben utilizar excepciones para controlar el flujo "normal" de la rutina. ¿Actuación? ¿La propagación de un error será más rápida que if / else?
Jakub M.
2
Depende de la probabilidad de que uses uno u otro.
2rs2ts
31
@JakubM. Las excepciones pueden, deben usarse y se usan así en Python.
Gareth Latty
13
Teniendo en cuenta que el forbucle de Python sale al detectar un error de StopIteration arrojado por el iterador en el que está pasando, diría que usar excepciones para el control de flujo es completamente Pythonic.
Kirk Strauser
1
Suponiendo que targetes Nonecuando se pretende sys.stdout, necesita capturar en TypeErrorlugar de IOError.
torek
5

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

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

Una mejora de la respuesta de Wolph.

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 IO binario y pasar eventuales argumentos extraños a opensi de filenamehecho es un nombre de archivo.

Evpok
fuente
1

También optaría por 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
Tommi Komulainen
fuente
Esta solución no cierra explícitamente el archivo en la terminación normal o por error de la cláusula with, por lo que no es un gran administrador de contexto. Una clase que implemente entrar y salir sería una mejor opción.
tdelaney
1
Me sale ValueError: I/O operation on closed filesi intento escribir en el archivo fuera del with open_or_stdout(..)bloque. ¿Qué me estoy perdiendo? sys.stdout no está destinado a cerrarse.
Tommi Komulainen
1

De acuerdo, si nos estamos metiendo en guerras de una sola línea, aquí está:

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

Me gusta el ejemplo original de Jacob siempre y cuando el contexto esté escrito en un solo lugar. Sería un problema si vuelve a abrir 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)
tdelaney
fuente
No creo que tu frase única cierre el objeto de archivo. ¿Me equivoco?
2rs2ts
1
@ 2rs2ts - Lo hace ... condicionalmente. El recuento de referencias del objeto de archivo va a cero porque no hay variables que apunten a él, 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 basura. 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 se ejecuta durante mucho tiempo y abre muchos archivos ... bueno, supongo que usaría 'con' o 'probar / finalmente'.
tdelaney
TIL. No sabía que los objetos de archivo __del__harían eso.
2rs2ts
@ 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 identificador de secuencia. Jython y aparentemente IronPython solo tienen el GC "real", por lo que no cierran el archivo hasta un eventual GC.
torek
0

Si realmente debes insistir en algo más "elegante", es decir, una frase:

>>> import sys
>>> target = "foo.txt"
>>> content = "foo"
>>> (lambda target, content: (lambda target, content: filter(lambda h: not h.write(content), (target,))[0].close())(open(target, 'w'), content) if target else sys.stdout.write(content))(target, content)

foo.txtaparece y contiene el texto foo.

2rs2ts
fuente
Esto debería moverse a CodeGolf StackExchange: D
kaiser
0

¿Qué tal abrir un nuevo fd para sys.stdout? De esta forma no tendrás problemas para cerrarlo:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
fuente
1
Lamentablemente, ejecutar este script de Python necesita un sudo en mi instalación. / dev / stdout es propiedad de root.
Manur
En muchas situaciones, volver a abrir un fd en stdout no es lo que se espera. Por ejemplo, este código truncará stdout, lo que hará que el shell ./script.py >> file sobrescriba el archivo en lugar de agregarlo.
salicideblock
Esto no funcionará en Windows que no tenga / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Ligera mejora en algunos casos.

Eugene K
fuente
0
import contextlib
import sys

with contextlib.ExitStack() as stack:
    h = stack.enter_context(open(target, 'w')) if target else sys.stdout
    h.write(content)

Solo dos líneas adicionales si usa Python 3.3 o superior: una línea para el extra importy una línea para el stack.enter_context.

Romanows
fuente
0

Si está bien que sys.stdoutesté cerrado después del withcuerpo, también puede usar patrones como este:

# Use stdout when target is "-"
with open(target, "w") if target != "-" else sys.stdout as f:
    f.write("hello world")

# Use stdout when target is falsy (None, empty string, ...)
with open(target, "w") if target else sys.stdout as f:
    f.write("hello world")

o incluso más en general:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Stefaan
fuente