Как правильно работать с `with open (…)` и `sys.stdout`?


93

Часто мне нужно вывести данные либо в файл, либо, если файл не указан, в стандартный вывод. Я использую следующий фрагмент:

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

Я бы хотел его переписать и обрабатывать обе цели единообразно.

В идеальном случае это будет:

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

но это не сработает, потому что sys.stdout закрывается при выходе из withблока, а я этого не хочу. Я не хочу

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

потому что мне нужно не забыть восстановить исходный стандартный вывод.

Связанный:

редактировать

Я знаю, что могу обернуть target, определить отдельную функцию или использовать диспетчер контекста . Я ищу простое, элегантное, идиоматическое решение, для которого не потребуется более 5 строк.


Жаль, что вы не добавили правку раньше;) В любом случае ... в качестве альтернативы вы можете просто не беспокоиться об очистке вашего открытого файла: P
Wolph

Ответы:


93

Просто мыслите нестандартно, как насчет специального open()метода?

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

Используйте это так:

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

29

Придерживайтесь своего текущего кода. Это просто, и вы можете точно сказать , что он делает, просто взглянув на него.

Другой способ - встроенный if:

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

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

Но это ненамного короче того, что у вас есть, и выглядит, возможно, хуже.

Вы также можете сделать его sys.stdoutнезакрываемым, но это не кажется слишком Pythonic:

sys.stdout.close = lambda: None

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

2
Вы можете сохранять незакрываемость столько времени, сколько вам нужно, создав для нее также диспетчер контекста: with unclosable(sys.stdout): ...установив sys.stdout.close = lambda: Noneвнутри этого диспетчера контекста и затем сбросив его на старое значение. Но это кажется слишком надуманным ...
glglgl

3
Я разрываюсь между голосованием за «оставьте это, вы можете точно сказать, что он делает» и голосованием за ужасное предложение, не подлежащее закрытию!
GreenAsJade

@GreenAsJade Я не думаю, что он предлагал сделать sys.stdoutнезащищенным, просто отмечая, что это можно сделать. Лучше показать плохие идеи и объяснить, почему они плохие, чем не упоминать их и надеяться, что на них не наткнутся другие.
cjs

8

Почему LBYL, если можно EAFP?

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

Зачем переписывать его для единообразного использования блока with/, asесли вам нужно заставить его работать запутанным образом? Вы добавите больше строк и снизите производительность.


3
Исключения не следует использовать для контроля «нормального» выполнения процедуры. Производительность? будет ли всплывать ошибка быстрее, чем if / else?
Jakub M.

2
Зависит от вероятности того, что вы будете использовать тот или иной.
2rs2ts

31
@JakubM. Исключения могут, должны и используются в Python подобным образом.
Gareth Latty

13
Учитывая, что forцикл Python завершается перехватом ошибки StopIteration, вызванной итератором, через который он проходит, я бы сказал, что использование исключений для управления потоком - это исключительно Pythonic.
Kirk Strauser

1
Предполагая, что targetэто Noneкогда предназначен sys.stdout, вам нужно поймать, TypeErrorа не IOError.
torek

5

Другое возможное решение: не пытайтесь избегать метода выхода из контекстного менеджера, просто продублируйте стандартный вывод.

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

5

Улучшение ответа Вольфа

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

Это позволяет выполнять двоичный ввод-вывод и передавать возможные посторонние аргументы в open если filenameдействительно имя файла.


1

Я бы также выбрал простую функцию-оболочку, которая может быть довольно простой, если вы можете игнорировать режим (и, следовательно, stdin vs. stdout), например:

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

Это решение не закрывает файл явно ни при нормальном, ни при ошибочном завершении предложения with, так что это не очень похоже на диспетчер контекста. Класс, реализующий вход и выход, был бы лучшим выбором.
tdelaney

1
У меня получится, ValueError: I/O operation on closed fileесли я попытаюсь записать в файл вне with open_or_stdout(..)блока. Что мне не хватает? sys.stdout не предназначен для закрытия.
Tommi Komulainen

1

Хорошо, если мы вступаем в однострочные войны, вот:

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

Мне нравится исходный пример Джейкоба, поскольку контекст написан только в одном месте. Было бы проблемой, если бы вы в конечном итоге повторно открывали файл для множества операций записи. Думаю, я бы просто принял решение один раз в верхней части скрипта и позволил системе закрыть файл при выходе:

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

Вы можете включить свой собственный обработчик выхода, если считаете его более удобным.

import atexit

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

atexit(cleanup_output)

Я не думаю, что ваш однострочный файл закрывает объект файла. Я ошибся?
2rs2ts

1
@ 2rs2ts - Есть ... условно. Refcount файлового объекта становится равным нулю, потому что на него не указывают никакие переменные, поэтому его метод __del__ может вызываться либо сразу (в cpython), либо позже, когда происходит сборка мусора. В документе есть предупреждения, чтобы не верить, что это всегда будет работать, но я использую его все время в более коротких сценариях. Что-то большое, что работает долго и открывает много файлов ... ну, думаю, я бы использовал 'with' или 'try / finally'.
tdelaney

TIL. Я не знал, что файловые объекты __del__будут делать это.
2rs2ts

@ 2rs2ts: CPython использует сборщик мусора с подсчетом ссылок (с «настоящим» сборщиком мусора, вызываемым по мере необходимости), поэтому он может закрыть файл, как только вы отбросите все ссылки на дескриптор потока. Jython и, по-видимому, IronPython имеют только «настоящий» сборщик мусора, поэтому они не закрывают файл до возможного сборщика мусора.
torek

0

Если вы действительно должны настоять на чем-то более "элегантном", например, на однострочном:

>>> 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.txtпоявляется и содержит текст foo.


Это должно быть перемещено в CodeGolf StackExchange: D
kaiser

0

Как насчет открытия нового fd для sys.stdout? Таким образом, у вас не возникнет проблем с его закрытием:

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

1
К сожалению, для запуска этого скрипта на Python требуется sudo при установке. / dev / stdout принадлежит root.
Манур

Во многих ситуациях повторное открытие fd на стандартный вывод не является ожидаемым. Например, этот код усечет стандартный вывод, таким образом заставляя оболочку ./script.py >> file перезаписывать файл вместо добавления к нему.
salicideblock

Это не будет работать в окнах, в которых нет / dev / stdout.
Брайан Окли,

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

В некоторых случаях небольшое улучшение.


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)

Всего две дополнительные строки, если вы используете Python 3.3 или выше: одна строка для дополнительных importи одна строка для stack.enter_context.


0

Если это нормально, что sys.stdoutзакрыто после withтела, вы также можете использовать такие шаблоны:

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

или даже в более общем плане:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.