Jak ładnie obsługiwać zarówno `with open (…)`, jak i `sys.stdout`?

93

Często muszę wyprowadzać dane do pliku lub, jeśli plik nie jest określony, do standardowego wyjścia. Używam następującego fragmentu:

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

Chciałbym go przepisać i jednolicie obsługiwać oba cele.

W idealnym przypadku byłoby to:

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

ale to nie zadziała dobrze, ponieważ sys.stdout jest zamykany przy opuszczaniu withbloku, a ja tego nie chcę. Ja też nie chcę

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

ponieważ musiałbym pamiętać o przywróceniu oryginalnego standardowego wyjścia.

Związane z:

Edytować

Wiem, że potrafię zawijać target, definiować oddzielną funkcję lub używać menedżera kontekstu . Szukam prostego, eleganckiego, idiomatycznego rozwiązania, które nie wymagałoby więcej niż 5 linek

Jakub M.
źródło
Szkoda, że ​​nie dodałeś edycji wcześniej;) W każdym razie ... alternatywnie możesz po prostu nie zawracać sobie głowy czyszczeniem otwartego pliku: P
Wolph

Odpowiedzi:

93

Po prostu myślisz nieszablonowo, co powiesz na niestandardową open()metodę?

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

Użyj tego w ten sposób:

# 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
źródło
29

Trzymaj się obecnego kodu. To proste i możesz dokładnie stwierdzić , co robi, po prostu na to spojrzawszy.

Innym sposobem byłoby użycie inline if:

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

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

Ale to nie jest dużo krótsze niż to, co masz i wygląda prawdopodobnie gorzej.

Możesz również sprawić sys.stdout, że zamkniesz się, ale to nie wydaje się zbyt Pythonowe:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
Mikser
źródło
2
Możesz zachować niezamknięcie tak długo, jak potrzebujesz, tworząc dla niego również menedżera kontekstu: with unclosable(sys.stdout): ...ustawiając sys.stdout.close = lambda: Nonewewnątrz tego menedżera kontekstu, a następnie resetując go do starej wartości. Ale to wydaje się trochę zbyt daleko idące ...
glglgl
3
Jestem rozdarty między głosowaniem za „zostaw to, możesz dokładnie powiedzieć, co robi” a głosowaniem za okropną niemożliwą do zamknięcia sugestią!
GreenAsJade
@GreenAsJade Nie sądzę, żeby sugerował tworzenie możliwości sys.stdoutzamknięcia, po prostu zauważył, że można to zrobić. Lepiej jest pokazać złe pomysły i wyjaśnić, dlaczego są złe, niż nie wspominać o nich i mieć nadzieję, że nie natkną się na nie inni.
cjs
8

Dlaczego LBYL, skoro możesz EFF?

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

Po co przepisywać go tak, aby używał jednolicie with/ asblock, kiedy trzeba sprawić, by działał w zawiły sposób? Dodasz więcej linii i zmniejszysz wydajność.

2rs2ts
źródło
3
Wyjątki nie powinny być używane do kontrolowania „normalnego” przebiegu procedury. Występ? czy bulgotanie błędu będzie szybsze niż jeśli / else?
Jakub M.
2
Zależy od prawdopodobieństwa, że ​​będziesz używać jednego lub drugiego.
2rs2ts,
31
@JakubM. Wyjątki mogą, powinny być i są używane w Pythonie w ten sposób.
Gareth Latty,
13
Biorąc pod uwagę, że forpętla Pythona kończy pracę, przechwytując błąd StopIteration wyrzucony przez iterator, przez który przechodzi, powiedziałbym, że używanie wyjątków do sterowania przepływem jest całkowicie Pythonowe.
Kirk Strauser,
1
Zakładając, że targetjest Noneto przeznaczone dla sys.stdout, musisz TypeErrorraczej złapać niż IOError.
torek
5

Inne możliwe rozwiązanie: nie próbuj unikać metody wyjścia menedżera kontekstu, po prostu skopiuj standardowe wyjście.

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

Poprawa odpowiedzi Wolpha

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

Pozwala to na binarne We / Wy i przekazywanie ewentualnych dodatkowych argumentów do openif filenamejest rzeczywiście nazwą pliku.

Evpok
źródło
1

Wybrałbym również prostą funkcję opakowującą, która może być całkiem prosta, jeśli możesz zignorować tryb (iw konsekwencji stdin vs. stdout), na przykład:

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
źródło
To rozwiązanie nie zamyka jawnie pliku ani przy normalnym, ani błędnym zakończeniu klauzuli with, więc nie jest to zbyt duży menedżer kontekstu. Klasa, która implementuje wejście i wyjście, byłaby lepszym wyborem.
tdelaney,
1
Dostaję się, ValueError: I/O operation on closed filejeśli spróbuję pisać do pliku poza with open_or_stdout(..)blokiem. czego mi brakuje? sys.stdout nie jest przeznaczone do zamknięcia.
Tommi Komulainen
1

W porządku, jeśli wchodzimy w wojny jednowierszowe, oto:

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

Podoba mi się oryginalny przykład Jacoba, o ile kontekst jest zapisany tylko w jednym miejscu. Byłoby to problemem, gdybyś ponownie otworzył plik dla wielu zapisów. Myślę, że podjąłbym decyzję raz na początku skryptu i pozwoliłbym systemowi zamknąć plik przy wyjściu:

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

Możesz dołączyć własny program obsługi wyjścia, jeśli uważasz, że jest bardziej uporządkowany

import atexit

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

atexit(cleanup_output)
tdelaney
źródło
Nie sądzę, żeby twoja jedna linijka zamknęła obiekt pliku. Czy się mylę?
2rs2ts,
1
@ 2rs2ts - Tak jest ... warunkowo. Refcount obiektu pliku spada do zera, ponieważ nie ma wskazujących na niego zmiennych, więc można wywołać metodę __del__ natychmiast (w cpythonie) lub później, gdy nastąpi czyszczenie pamięci. W dokumencie są ostrzeżenia, aby nie ufać, że to zawsze zadziała, ale używam go cały czas w krótszych skryptach. Coś dużego, które działa długo i otwiera wiele plików ... cóż, myślę, że użyłbym „z” lub „spróbuj / w końcu”.
tdelaney,
TIL. Nie wiedziałem, że obiekty plików __del__to zrobią.
2rs2ts
@ 2rs2ts: CPython używa garbage collectora zliczającego odwołania (z "prawdziwym" GC pod wywołanym w razie potrzeby), więc może zamknąć plik, gdy tylko usuniesz wszystkie odniesienia do uchwytu strumienia. Jython i najwyraźniej IronPython mają tylko „prawdziwą” GC, więc nie zamykają pliku przed ostatecznym GC.
torek 12.07.13
0

Jeśli naprawdę musisz nalegać na coś bardziej „eleganckiego”, tj. Jednolinijkowy:

>>> 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.txtpojawia się i zawiera tekst foo.

2rs2ts
źródło
To powinno zostać przeniesione do CodeGolf StackExchange: D
kaiser
0

Co powiesz na otwarcie nowego fd dla sys.stdout? W ten sposób nie będziesz mieć żadnych problemów z zamknięciem go:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
źródło
1
Niestety, uruchomienie tego skryptu Pythona wymaga sudo w mojej instalacji. / dev / stdout jest własnością roota.
Manur
W wielu sytuacjach ponowne otwarcie pliku fd na stdout nie jest tym, czego się oczekuje. Na przykład, ten kod obetnie standardowe wyjście, co spowoduje, że takie rzeczy jak powłoka ./script.py >> file nadpiszą plik zamiast dołączać do niego.
blok salicydowy
To nie zadziała w oknach, które nie mają / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Nieznaczna poprawa w niektórych przypadkach.

Eugene K.
źródło
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)

Tylko dwie dodatkowe linie, jeśli używasz Pythona 3.3 lub nowszego: jedna linia dla dodatkowej importi jedna dla stack.enter_context.

romanows
źródło
0

Jeśli jest w porządku, że sys.stdoutjest zamykany po withciele, możesz również użyć takich wzorów:

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

lub jeszcze bardziej ogólnie:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Stefaan
źródło