Bagaimana cara menangani `dengan open (…)` dan `sys.stdout` dengan baik?

93

Seringkali saya perlu mengeluarkan data ke file atau, jika file tidak ditentukan, ke stdout. Saya menggunakan potongan berikut:

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

Saya ingin menulis ulang dan menangani kedua target secara seragam.

Dalam kasus yang ideal itu adalah:

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

tetapi ini tidak akan berfungsi dengan baik karena sys.stdout ditutup saat keluar dari withblok dan saya tidak menginginkannya. Saya juga tidak mau

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

karena saya harus ingat untuk mengembalikan stdout asli.

Terkait:

Edit

Saya tahu bahwa saya dapat membungkus target, menentukan fungsi terpisah atau menggunakan manajer konteks . Saya mencari solusi yang sederhana, elegan, dan idiomatis yang tidak memerlukan lebih dari 5 baris

Jakub M.
sumber
Sayang sekali Anda tidak menambahkan suntingan sebelumnya;) Bagaimanapun ... sebagai alternatif Anda tidak dapat repot-repot membersihkan file yang terbuka: P
Wolph

Jawaban:

93

Hanya berpikir di luar kebiasaan di sini, bagaimana dengan open()metode khusus ?

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

Gunakan seperti ini:

# 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
sumber
29

Tetap gunakan kode Anda saat ini. Sederhana dan Anda dapat mengetahui dengan tepat apa yang dilakukannya hanya dengan melihatnya sekilas.

Cara lain adalah dengan inline if:

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

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

Tapi itu tidak jauh lebih pendek dari yang Anda miliki dan terlihat lebih buruk.

Anda juga bisa membuat sys.stdoutunclosable, tapi itu sepertinya tidak terlalu Pythonic:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
Blender
sumber
2
Anda dapat menjaga agar tidak dapat ditutup selama Anda membutuhkannya dengan membuat pengelola konteks untuk itu juga: with unclosable(sys.stdout): ...dengan menyetel sys.stdout.close = lambda: Nonedi dalam pengelola konteks ini dan menyetel ulang ke nilai lama sesudahnya. Tapi ini sepertinya terlalu dibuat-buat ...
glglgl
3
Saya terpecah antara memberi suara untuk "tinggalkan, Anda dapat mengetahui dengan tepat apa yang dilakukannya" dan memilih saran yang tidak dapat ditutup!
GreenAsJade
@GreenAsJade Saya tidak berpikir bahwa dia menyarankan untuk membuat tidak dapat sys.stdoutditutup, hanya mencatat bahwa itu bisa dilakukan. Lebih baik menunjukkan ide-ide buruk dan menjelaskan mengapa ide-ide itu buruk daripada tidak menyebutkannya dan berharap ide-ide itu tidak tersandung oleh orang lain.
cjs
8

Mengapa LBYL saat Anda bisa EAFP?

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

Mengapa menulis ulang untuk menggunakan blok with/ assecara seragam ketika Anda harus membuatnya bekerja dengan cara yang berbelit-belit? Anda akan menambahkan lebih banyak garis dan mengurangi kinerja.

2rs2ts
sumber
3
Pengecualian tidak boleh digunakan untuk mengontrol aliran "normal" dari rutinitas. Performa? akankah meluapkan kesalahan lebih cepat dari if / else?
Jakub M.
2
Tergantung pada kemungkinan bahwa Anda akan menggunakan salah satunya.
2rs2ts
31
@Bayu_joo Pengecualian dapat, harus, dan digunakan seperti ini di Python.
Gareth Latty
13
Mempertimbangkan bahwa forloop Python keluar dengan menangkap kesalahan StopIteration yang dilemparkan oleh iterator yang dilewatinya, saya akan mengatakan bahwa menggunakan pengecualian untuk kontrol aliran sepenuhnya Pythonic.
Kirk Strauser
1
Dengan asumsi bahwa targetadalah Noneketika sys.stdout dimaksudkan, Anda perlu untuk menangkap TypeErrorbukan IOError.
Torek
5

Solusi lain yang mungkin: jangan mencoba untuk menghindari metode keluar dari manajer konteks, cukup duplikat stdout.

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

Peningkatan dari jawaban 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

Hal ini memungkinkan IO biner dan meneruskan argumen asing yang akhirnya openjika filenamememang berupa nama file.

Evpok
sumber
1

Saya juga akan menggunakan fungsi pembungkus sederhana, yang bisa sangat sederhana jika Anda dapat mengabaikan mode (dan akibatnya stdin vs. stdout), misalnya:

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
sumber
Solusi ini tidak secara eksplisit menutup file baik pada penghentian normal atau kesalahan klausa with sehingga tidak banyak manajer konteks. Kelas yang menerapkan masuk dan keluar akan menjadi pilihan yang lebih baik.
tdelaney
1
Saya mendapatkan ValueError: I/O operation on closed filejika saya mencoba menulis ke file di luar with open_or_stdout(..)blok. Apa yang saya lewatkan? sys.stdout tidak dimaksudkan untuk ditutup.
Tommi Komulainen
1

Oke, jika kita terlibat dalam perang satu baris, berikut ini:

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

Saya suka contoh asli Yakub asalkan konteksnya hanya ditulis di satu tempat. Ini akan menjadi masalah jika Anda akhirnya membuka kembali file untuk banyak penulisan. Saya pikir saya hanya akan membuat keputusan sekali di bagian atas skrip dan membiarkan sistem menutup file saat keluar:

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

Anda dapat memasukkan penangan keluar Anda sendiri jika menurut Anda itu lebih rapi

import atexit

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

atexit(cleanup_output)
tdelaney.dll
sumber
Saya tidak berpikir satu baris Anda menutup objek file. Apakah aku salah?
2rs2ts
1
@ 2rs2ts - Memang ... secara kondisional. Refcount objek file menjadi nol karena tidak ada variabel yang menunjuk ke sana, sehingga metode __del__ dapat dipanggil langsung (dalam cpython) atau nanti ketika pengumpulan sampah terjadi. Ada peringatan di dokumen untuk tidak mempercayai bahwa ini akan selalu berhasil tetapi saya menggunakannya sepanjang waktu dalam skrip yang lebih pendek. Sesuatu yang besar yang berjalan lama dan membuka banyak file ... saya rasa saya akan menggunakan 'dengan' atau 'coba / akhirnya'.
tdelaney
TIL. Saya tidak tahu bahwa objek file ' __del__akan melakukan itu.
2rs2ts
@ 2rs2ts: CPython menggunakan pengumpul sampah penghitungan referensi (dengan GC "nyata" di bawahnya dipanggil sesuai kebutuhan) sehingga CPython dapat menutup file segera setelah Anda melepaskan semua referensi ke pegangan-aliran. Jython dan tampaknya IronPython hanya memiliki GC "asli" sehingga mereka tidak menutup file sampai akhirnya ada GC.
Torek
0

Jika Anda benar-benar harus menuntut sesuatu yang lebih "elegan", yaitu satu kalimat:

>>> 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.txtmuncul dan berisi teks foo.

2rs2ts
sumber
Ini harus dipindahkan ke CodeGolf StackExchange: D
kaiser
0

Bagaimana jika membuka fd baru untuk sys.stdout? Dengan cara ini Anda tidak akan kesulitan menutupnya:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
pengguna2602746
sumber
1
Sayangnya, menjalankan skrip python ini membutuhkan sudo pada pemasangan saya. / dev / stdout dimiliki oleh root.
Manur
Dalam banyak situasi, membuka kembali fd ke stdout bukanlah yang diharapkan. Misalnya, kode ini akan memotong stdout, sehingga membuat hal-hal shell seperti ./script.py >> file menimpa file daripada menambahkannya.
salicideblock
Ini tidak akan berfungsi pada windows yang tidak memiliki / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Sedikit peningkatan dalam beberapa kasus.

Eugene K
sumber
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)

Hanya dua baris tambahan jika Anda menggunakan Python 3.3 atau lebih tinggi: satu baris untuk ekstra importdan satu baris untuk stack.enter_context.

romanows
sumber
0

Jika fine yang sys.stdoutditutup after withbody, Anda juga bisa menggunakan pola seperti ini:

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

atau bahkan lebih umum:

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