Как обращаться с `` с открытым (...) `и` sys.stdout` красиво?

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

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')
...

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

Связанный:

Edit

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

Ответ 1

Просто думая вне поля здесь, как насчет пользовательского метода 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()

Используйте его следующим образом:

# writes to some_file
with smart_open('some_file') as fh:
    print >>fh, 'some output'

# writes to stdout
with smart_open() as fh:
    print >>fh, 'some output'

# writes to stdout
with smart_open('-') as fh:
    print >>fh, 'some output'

Ответ 2

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

Другой способ - с встроенным 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)

Ответ 3

Почему LBYL, когда вы можете использовать EAFP?

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

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

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

Я также хотел бы использовать простую функцию-обертку, которая может быть довольно простой, если вы можете игнорировать режим (и, следовательно, 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

Ответ 7

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

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

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

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)

Ответ 8

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

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

Ответ 9

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

>>> 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.

Ответ 10

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

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