Wie geht man sowohl mit open (…) als auch mit sys.stdout gut um?

91

Oft muss ich Daten entweder in eine Datei oder, wenn keine Datei angegeben ist, in stdout ausgeben. Ich benutze das folgende Snippet:

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

Ich möchte es umschreiben und beide Ziele einheitlich behandeln.

Im Idealfall wäre es:

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

Dies funktioniert jedoch nicht gut, da sys.stdout beim Verlassen des withBlocks geschlossen wird und ich das nicht möchte. Ich will auch nicht

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

weil ich daran denken müsste, das ursprüngliche stdout wiederherzustellen.

Verbunden:

Bearbeiten

Ich weiß, dass ich einschließen target, separate Funktionen definieren oder den Kontextmanager verwenden kann . Ich suche eine einfache, elegante, idiomatische Lösung, die nicht mehr als 5 Zeilen benötigt

Jakub M.
quelle
Schade, dass Sie die Bearbeitung nicht früher hinzugefügt haben;) Wie auch immer ... alternativ können Sie sich einfach nicht die Mühe machen, Ihre geöffnete Datei zu bereinigen: P
Wolph

Antworten:

92

Wenn Sie hier nur über den Tellerrand hinaus denken, wie wäre es mit einer benutzerdefinierten open()Methode?

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

Verwenden Sie es so:

# 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
quelle
27

Halten Sie sich an Ihren aktuellen Code. Es ist einfach und Sie können genau sagen , was es tut, indem Sie es sich ansehen.

Ein anderer Weg wäre mit einem Inline if:

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

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

Aber das ist nicht viel kürzer als das, was Sie haben, und es sieht wohl schlimmer aus.

Sie könnten auch nicht sys.stdoutverschließbar machen , aber das scheint nicht zu pythonisch:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
Mixer
quelle
1
Sie können die Unschließbarkeit so lange beibehalten, wie Sie sie benötigen, indem Sie auch einen Kontextmanager dafür erstellen: with unclosable(sys.stdout): ...indem Sie sys.stdout.close = lambda: Nonein diesem Kontextmanager festlegen und ihn anschließend auf den alten Wert zurücksetzen. Aber das scheint ein bisschen zu weit hergeholt ...
glglgl
3
Ich bin hin- und hergerissen zwischen der Abstimmung für "Lass es, du kannst genau sagen, was es tut" und der Abstimmung für den schrecklichen, nicht abschließbaren Vorschlag!
GreenAsJade
8

Warum LBYL, wenn Sie EAFP können?

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

Warum sollten Sie es umschreiben, um den with/ asblock einheitlich zu verwenden, wenn Sie dafür sorgen müssen, dass er verschlungen funktioniert? Sie fügen mehr Zeilen hinzu und reduzieren die Leistung.

2rs2ts
quelle
3
Ausnahmen sollten nicht verwendet werden, um den "normalen" Ablauf der Routine zu steuern. Performance? Wird das Sprudeln eines Fehlers schneller sein als wenn / sonst?
Jakub M.
2
Hängt von der Wahrscheinlichkeit ab, dass Sie das eine oder andere verwenden.
2rs2ts
31
@ JakubM. Ausnahmen können, sollten und werden in Python so verwendet.
Gareth Latty
13
In Anbetracht der Tatsache, dass die Python- forSchleife beendet wird, indem ein StopIteration-Fehler abgefangen wird, der von dem Iterator ausgelöst wird, über den er sich schleift, würde ich sagen, dass die Verwendung von Ausnahmen für die Flusssteuerung absolut pythonisch ist.
Kirk Strauser
1
Dass unter der Annahme targetist , Nonewenn sys.stdout vorgesehen ist, müssen Sie fangen , TypeErrorstatt IOError.
Torek
5

Eine andere mögliche Lösung: Versuchen Sie nicht, die Exit-Methode des Kontextmanagers zu vermeiden, sondern duplizieren Sie stdout.

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

Eine Verbesserung von Wolphs Antwort

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

Dies ermöglicht binäre E / A und übergibt eventuelle irrelevante Argumente an openif , wenn filenamees sich tatsächlich um einen Dateinamen handelt.

Evpok
quelle
1

Ich würde mich auch für eine einfache Wrapper-Funktion entscheiden, die ziemlich einfach sein kann, wenn Sie den Modus (und folglich stdin vs. stdout) ignorieren können, zum Beispiel:

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
quelle
Diese Lösung schließt die Datei weder bei normaler noch bei fehlerhafter Beendigung der with-Klausel explizit, sodass sie kein großer Kontextmanager ist. Eine Klasse, die Enter und Exit implementiert , wäre die bessere Wahl.
tdelaney
1
Ich bekomme, ValueError: I/O operation on closed filewenn ich versuche, in die Datei außerhalb des with open_or_stdout(..)Blocks zu schreiben . Was vermisse ich? sys.stdout soll nicht geschlossen werden.
Tommi Komulainen
1

Okay, wenn wir in Einzeilerkriege geraten, hier ist:

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

Ich mag Jacobs ursprüngliches Beispiel, solange der Kontext nur an einer Stelle geschrieben ist. Es wäre ein Problem, wenn Sie die Datei für viele Schreibvorgänge erneut öffnen würden. Ich denke, ich würde die Entscheidung nur einmal oben im Skript treffen und das System die Datei beim Beenden schließen lassen:

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

Sie können Ihren eigenen Exit-Handler einbinden, wenn Sie der Meinung sind, dass er ordentlicher ist

import atexit

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

atexit(cleanup_output)
tdelaney
quelle
Ich glaube nicht, dass Ihr Einzeiler das Dateiobjekt schließt. Liege ich falsch?
2rs2ts
1
@ 2rs2ts - Es tut ... bedingt. Der Refcount des Dateiobjekts wird auf Null gesetzt, da keine Variablen darauf verweisen. Daher kann die Methode __del__ entweder sofort (in cpython) oder später aufgerufen werden, wenn die Speicherbereinigung erfolgt. Es gibt Warnungen im Dokument, nicht zu vertrauen, dass dies immer funktioniert, aber ich verwende es die ganze Zeit in kürzeren Skripten. Etwas Großes, das lange läuft und viele Dateien öffnet ... nun, ich denke, ich würde 'mit' oder 'versuchen / endlich' verwenden.
tdelaney
Bis. Ich wusste nicht, dass Dateiobjekte __del__das tun würden.
2rs2ts
@ 2rs2ts: CPython verwendet einen Garbage Collector mit Referenzzählung (mit einem "echten" GC darunter, der bei Bedarf aufgerufen wird), damit die Datei geschlossen werden kann, sobald Sie alle Verweise auf das Stream-Handle löschen. Jython und anscheinend IronPython haben nur den "echten" GC, sodass sie die Datei erst nach einem eventuellen GC schließen.
Torek
0

Wenn Sie wirklich auf etwas "Eleganterem" bestehen müssen, dh auf einem Einzeiler:

>>> 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.txterscheint und enthält den Text foo.

2rs2ts
quelle
Dies sollte zu CodeGolf Stack bewegt werden: D
kaiser
0

Wie wäre es mit einem neuen fd für sys.stdout? Auf diese Weise haben Sie keine Probleme beim Schließen:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
quelle
1
Leider benötigt das Ausführen dieses Python-Skripts ein Sudo für meine Installation. / dev / stdout gehört root.
Manur
In vielen Situationen ist das erneute Öffnen eines fd auf stdout nicht das, was erwartet wird. Zum Beispiel wird dieser Code stdout abschneiden, wodurch Shell-Dinge wie das ./script.py >> file Überschreiben der Datei entstehen, anstatt sie anzuhängen.
Salicideblock
Dies funktioniert nicht unter Windows ohne / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

In einigen Fällen leichte Verbesserung.

Eugene K.
quelle
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)

Nur zwei zusätzliche Zeilen, wenn Sie Python 3.3 oder höher verwenden: eine Zeile für die zusätzliche importund eine Zeile für die stack.enter_context.

Romanows
quelle