Làm cách nào để xử lý tốt cả `với open (…)` và `sys.stdout`?


92

Thường thì tôi cần xuất dữ liệu ra tệp hoặc, nếu tệp không được chỉ định, sang stdout. Tôi sử dụng đoạn mã sau:

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

Tôi muốn viết lại nó và xử lý thống nhất cả hai mục tiêu.

Trong trường hợp lý tưởng, nó sẽ là:

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

nhưng điều này sẽ không hoạt động tốt vì sys.stdout bị đóng khi rời khỏi withkhối và tôi không muốn điều đó. Tôi không muốn

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

bởi vì tôi cần phải nhớ khôi phục stdout ban đầu.

Có liên quan:

Biên tập

Tôi biết rằng tôi có thể bao bọc target, xác định chức năng riêng biệt hoặc sử dụng trình quản lý ngữ cảnh . Tôi tìm kiếm một giải pháp đơn giản, thanh lịch, phù hợp thành ngữ, không yêu cầu nhiều hơn 5 dòng


Quá xấu bạn không thêm các chỉnh sửa trước đó;) Nhưng dù sao ... cách khác bạn có thể đơn giản là không bận tâm đến việc dọn dẹp tập tin đang mở của bạn: P
Wolph

Câu trả lời:


92

Chỉ cần suy nghĩ bên ngoài hộp ở đây, làm thế nào về một open()phương pháp tùy chỉnh ?

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

Sử dụng nó như thế này:

# 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

Gắn bó với mã hiện tại của bạn. Nó đơn giản và bạn có thể biết chính xác nó đang làm gì chỉ bằng cách nhìn lướt qua nó.

Một cách khác sẽ là với nội tuyến if:

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

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

Nhưng nó không ngắn hơn nhiều so với những gì bạn có và nó có vẻ tệ hơn.

Bạn cũng có thể làm cho không thể sys.stdoutmở được, nhưng điều đó có vẻ không quá Pythonic:

sys.stdout.close = lambda: None

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

2
Bạn có thể giữ tính không kín đáo miễn là bạn cần bằng cách tạo trình quản lý ngữ cảnh cho nó: with unclosable(sys.stdout): ...bằng cách đặt sys.stdout.close = lambda: Nonebên trong trình quản lý ngữ cảnh này và đặt lại nó về giá trị cũ sau đó. Nhưng điều này có vẻ như một chút quá xa vời ...
glglgl

3
Tôi đang bị giằng xé giữa việc bỏ phiếu cho "hãy để nó đi, bạn có thể biết chính xác nó đang làm gì" và bỏ phiếu cho đề xuất kinh khủng không thể mở được!
GreenAsJade

@GreenAsJade Tôi không nghĩ rằng anh ấy đang đề nghị tạo ra những sys.stdoutthứ không thể mở được, chỉ cần lưu ý rằng điều đó có thể được thực hiện. Tốt hơn là bạn nên trình bày những ý tưởng tồi và giải thích tại sao chúng tệ hơn là không đề cập đến chúng và hy vọng rằng chúng không bị người khác bắt gặp.
cjs

8

Tại sao LBYL khi bạn có thể EAFP?

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

Tại sao phải viết lại nó để sử dụng thống nhất with/ asblock khi bạn phải làm cho nó hoạt động một cách phức tạp? Bạn sẽ thêm nhiều dòng và giảm hiệu suất.


3
Không nên sử dụng ngoại lệ để kiểm soát dòng chảy "bình thường" của quy trình. Hiệu suất? lỗi sủi bọt sẽ nhanh hơn nếu / khác?
Jakub M.

2
Phụ thuộc vào xác suất bạn sẽ sử dụng cái này hay cái kia.
2rs2ts

31
@JakubM. Các ngoại lệ có thể, nên và được sử dụng như thế này trong Python.
Gareth Latty

13
Xem xét rằng forvòng lặp của Python thoát ra bằng cách bắt lỗi StopIteration do trình lặp mà nó lặp lại, tôi muốn nói rằng việc sử dụng các ngoại lệ để kiểm soát luồng hoàn toàn là Pythonic.
Kirk Strauser,

1
Giả sử đó targetNonekhi sys.stdout được dự định, bạn cần phải nắm bắt TypeErrorhơn là IOError.
xé vào

5

Một giải pháp khả thi khác: đừng cố tránh phương thức thoát trình quản lý ngữ cảnh, chỉ cần sao chép stdout.

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

5

Một cải tiến cho câu trả lời của 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

Điều này cho phép IO nhị phân và chuyển các đối số không liên quan đến opennếu filenamethực sự là tên tệp.


1

Tôi cũng muốn sử dụng một hàm wrapper đơn giản, có thể khá đơn giản nếu bạn có thể bỏ qua chế độ (và do đó stdin so với stdout), ví dụ:

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

Giải pháp này không đóng tệp một cách rõ ràng khi bình thường hoặc kết thúc lỗi của mệnh đề with nên nó không có nhiều tác dụng quản lý ngữ cảnh. Một lớp thực hiện nhậpxuất sẽ là lựa chọn tốt hơn.
tdelaney

1
Tôi nhận được ValueError: I/O operation on closed filenếu tôi cố gắng ghi vào tệp bên ngoài with open_or_stdout(..)khối. Tôi đang thiếu gì? sys.stdout không có nghĩa là bị đóng.
Tommi Komulainen,

1

Được rồi, nếu chúng ta đang tham gia vào cuộc chiến một bên, đây là:

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

Tôi thích ví dụ ban đầu của Jacob miễn là ngữ cảnh chỉ được viết ở một nơi. Sẽ là một vấn đề nếu bạn mở lại tệp trong nhiều lần ghi. Tôi nghĩ rằng tôi sẽ chỉ đưa ra quyết định một lần ở đầu tập lệnh và để hệ thống đóng tệp khi thoát:

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

Bạn có thể bao gồm trình xử lý thoát của riêng mình nếu bạn nghĩ rằng nó gọn gàng hơn

import atexit

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

atexit(cleanup_output)

Tôi không nghĩ rằng một lớp lót của bạn đóng đối tượng tệp. Liệu tôi có sai?
2rs2ts

1
@ 2rs2ts - Nó ... có điều kiện. Tài khoản của đối tượng tệp về 0 vì không có biến nào trỏ đến nó, vì vậy nó có sẵn phương thức __del__ của nó được gọi ngay lập tức (trong cpython) hoặc sau đó khi quá trình thu gom rác xảy ra. Có những cảnh báo trong tài liệu đừng tin rằng điều này sẽ luôn hoạt động nhưng tôi sử dụng nó mọi lúc trong các tập lệnh ngắn hơn. Một cái gì đó lớn chạy trong thời gian dài và mở rất nhiều tệp ... tôi đoán tôi sẽ sử dụng 'with' hoặc 'try / Cuối cùng'.
tdelaney.

GẠCH. Tôi không biết rằng các đối tượng tệp __del__sẽ làm điều đó.
2rs2ts

@ 2rs2ts: CPython sử dụng bộ thu gom rác đếm tham chiếu (với GC "thực" bên dưới được gọi nếu cần) để nó có thể đóng tệp ngay sau khi bạn thả tất cả các tham chiếu đến xử lý luồng. Jython và dường như IronPython chỉ có GC "thực" nên họ không đóng tệp cho đến khi có GC cuối cùng.
xé vào

0

Nếu bạn thực sự phải nhấn mạnh vào một cái gì đó "thanh lịch" hơn, tức là một lớp lót:

>>> 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.txtxuất hiện và chứa văn bản foo.


Điều này nên được chuyển đến CodeGolf StackExchange: D
kaiser

0

Làm thế nào về việc mở một fd mới cho sys.stdout? Bằng cách này, bạn sẽ không gặp bất kỳ sự cố nào khi đóng nó:

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

1
Đáng buồn thay, việc chạy tập lệnh python này cần có sudo trong quá trình cài đặt của tôi. / dev / stdout được sở hữu bởi root.
Manur

Trong nhiều tình huống, việc mở lại fd to stdout không phải là điều mong đợi. Ví dụ: mã này sẽ cắt ngắn stdout, do đó làm cho những thứ của shell như ./script.py >> file ghi đè lên tệp thay vì thêm vào nó.
salicideblock

Điều này sẽ không hoạt động trên các cửa sổ không có / dev / stdout.
Bryan Oakley

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

Cải thiện nhẹ trong một số trường hợp.


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)

Chỉ cần thêm hai dòng nếu bạn đang sử dụng Python 3.3 trở lên: một dòng cho phần bổ sung importvà một dòng cho stack.enter_context.


0

Nếu nó sys.stdoutđược đóng sau withphần thân, bạn cũng có thể sử dụng các mẫu như sau:

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

hoặc thậm chí tổng quát hơn:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.