Come gestire bene sia `con open (...)` che `sys.stdout`?


92

Spesso ho bisogno di inviare i dati su file o, se il file non è specificato, su stdout. Uso il seguente frammento:

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

Vorrei riscriverlo e gestire entrambi gli obiettivi in ​​modo uniforme.

Nel caso ideale sarebbe:

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

ma questo non funzionerà bene perché sys.stdout è chiuso quando si lascia il withblocco e non lo voglio. Nemmeno io lo voglio

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

perché avrei bisogno di ricordarmi di ripristinare lo stdout originale.

Relazionato:

modificare

So che posso avvolgere target, definire funzioni separate o utilizzare il gestore di contesto . Cerco una soluzione semplice, elegante, idiomatica che non richieda più di 5 righe


Peccato che tu non abbia aggiunto la modifica prima;) Comunque ... in alternativa non puoi semplicemente preoccuparti di pulire il tuo file aperto: P
Wolph

Risposte:


93

Solo pensando fuori dagli schemi qui, che ne dici di un open()metodo personalizzato ?

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

Usalo in questo modo:

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

28

Attenersi al codice corrente. È semplice e puoi dire esattamente cosa sta facendo semplicemente guardandolo.

Un altro modo sarebbe con un inline if:

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

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

Ma non è molto più breve di quello che hai e sembra probabilmente peggio.

Potresti anche renderlo non sys.stdoutchiudibile, ma non sembra troppo pitonico:

sys.stdout.close = lambda: None

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

2
Puoi mantenere unclosability per tutto il tempo che ti serve creando un context manager anche per esso: with unclosable(sys.stdout): ...impostandolo sys.stdout.close = lambda: Noneall'interno di questo context manager e resettandolo al vecchio valore in seguito. Ma questo sembra un po 'troppo inverosimile ...
glglgl

3
Sono combattuto tra votare per "lascia perdere, puoi dire esattamente cosa sta facendo" e votare per l'orrendo suggerimento non chiudibile!
GreenAsJade

@GreenAsJade Non credo che stesse suggerendo di rendere non sys.stdoutchiudibile, solo notando che poteva essere fatto. È meglio mostrare cattive idee e spiegare perché sono cattive piuttosto che non menzionarle e sperare che non vengano inciampate da altri.
cjs

8

Perché LBYL quando puoi EAFP?

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

Perché riscriverlo per usare il blocco with/ in asmodo uniforme quando devi farlo funzionare in modo contorto? Aggiungerai più linee e ridurrai le prestazioni.


3
Le eccezioni non dovrebbero essere utilizzate per controllare il flusso "normale" della routine. Prestazione? il bubbling-up di un errore sarà più veloce che if / else?
Jakub M.

2
Dipende dalla probabilità che utilizzerai l'uno o l'altro.
2rs2ts

31
@JakubM. Le eccezioni possono, dovrebbero essere e vengono usate in questo modo in Python.
Gareth Latty

13
Considerando che il forciclo di Python esce rilevando un errore StopIteration generato dall'iteratore su cui sta eseguendo il ciclo, direi che l'uso delle eccezioni per il controllo del flusso è assolutamente Pythonic.
Kirk Strauser

1
Supponendo che targetsia Nonequando si intende sys.stdout, è necessario prendere TypeErrorpiuttosto che IOError.
torek

5

Un'altra possibile soluzione: non cercare di evitare il metodo di uscita del gestore di contesto, basta duplicare lo stdout.

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

5

Un miglioramento della risposta di 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

Ciò consente l'IO binario e passa eventuali argomenti estranei a openif filenameè effettivamente un nome di file.


1

Vorrei anche optare per una semplice funzione wrapper, che può essere piuttosto semplice se puoi ignorare la modalità (e di conseguenza stdin vs. stdout), ad esempio:

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

Questa soluzione non chiude in modo esplicito il file alla fine normale o per errore della clausola with, quindi non è molto un gestore di contesto. Una classe che implementa l' entrata e l' uscita sarebbe una scelta migliore.
tdelaney

1
Ottengo ValueError: I/O operation on closed filese provo a scrivere nel file fuori dal with open_or_stdout(..)blocco. Cosa mi sto perdendo? sys.stdout non è pensato per essere chiuso.
Tommi Komulainen

1

Ok, se stiamo entrando in guerre a una linea, ecco:

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

Mi piace l'esempio originale di Jacob fintanto che il contesto è scritto solo in un punto. Sarebbe un problema se dovessi riaprire il file per molte scritture. Penso che prenderei la decisione solo una volta all'inizio dello script e lascerei che il sistema chiudesse il file all'uscita:

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

Potresti includere il tuo gestore di uscita se ritieni che sia più ordinato

import atexit

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

atexit(cleanup_output)

Non credo che la tua battuta chiuda l'oggetto file. Ho sbagliato?
2rs2ts

1
@ 2rs2ts - Lo fa ... condizionatamente. Il refcount dell'oggetto file va a zero perché non ci sono variabili che puntano ad esso, quindi è disponibile per avere il suo metodo __del__ chiamato immediatamente (in cpython) o successivamente quando avviene la garbage collection. Ci sono avvertimenti nel documento di non fidarsi del fatto che funzionerà sempre, ma lo uso sempre negli script più brevi. Qualcosa di grande che dura da molto tempo e apre molti file ... beh, immagino che userei "con" o "prova / finalmente".
tdelaney

TIL. Non sapevo che gli oggetti file 'lo __del__avrebbero fatto.
2rs2ts

@ 2rs2ts: CPython utilizza un garbage collector per il conteggio dei riferimenti (con un GC "reale" sotto richiamato secondo necessità) in modo che possa chiudere il file non appena si rilasciano tutti i riferimenti allo stream-handle. Jython e apparentemente IronPython hanno solo il "vero" GC, quindi non chiudono il file fino a un eventuale GC.
torek

0

Se proprio devi insistere su qualcosa di più "elegante", cioè una battuta:

>>> 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.txtappare e contiene il testo foo.


Dovrebbe essere spostato in CodeGolf StackExchange: D
kaiser

0

Che ne dici di aprire un nuovo fd per sys.stdout? In questo modo non avrai problemi a chiuderlo:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)

1
Purtroppo, l'esecuzione di questo script python richiede un sudo sulla mia installazione. / dev / stdout è di proprietà di root.
Manur

In molte situazioni, riaprire un fd in stdout non è ciò che ci si aspetta. Ad esempio, questo codice troncerà lo stdout, rendendo così la shell cose come ./script.py >> file sovrascrivere il file invece di aggiungervi.
salicideblock

Questo non funzionerà su Windows che non ha / dev / stdout.
Bryan Oakley

0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Leggero miglioramento in alcuni casi.


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 due righe extra se stai usando Python 3.3 o versioni successive: una riga per l'extra importe una riga per il stack.enter_context.


0

Se va bene che sys.stdoutè chiuso dopo il withcorpo, puoi anche usare modelli come questo:

# 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 anche più in generale:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.