Как правильно работать с `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если вам нужно заставить его работать запутанным образом? Вы добавите больше строк и снизите производительность.

2рс2ц
источник
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.

2рс2ц
источник
Это должно быть перемещено в CodeGolf StackExchange: D
kaiser
0

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

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
источник
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")
Стефаан
источник