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


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

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

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

по теме:

Edit

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

10 70

10 ответов:

просто думая вне коробки здесь, как насчет обычая 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'

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

другой способ был бы со встроенным if:

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

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

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

вы также можете сделать sys.stdout unclosable, но это не слишком подходящие для Python:

sys.stdout.close = lambda: None

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

почему LBYL, когда вы можете EAFP?

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

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

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

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

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

улучшение Wolph это!--5-->

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename: str = None, mode: str = 'r', *args, **kwargs):
    if filename == '-':
        if 'r' in mode:
            stream = sys.stdin
        else:
            stream = sys.stdout
        if 'b' in mode:
            fh = stream.buffer
        else:
            fh = stream
    else:
        fh = open(filename, mode, *args, **kwargs)

    try:
        yield fh
    finally:
        try:
            fh.close()
        except AttributeError:
            pass

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

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

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

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

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

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

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

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

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