Як добре обробляти і `з відкритим (...)`, і `sys.stdout`?


92

Часто мені потрібно виводити дані або у файл, або, якщо файл не вказаний, у 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.

Пов’язані:

Редагувати

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


Шкода, що ви не додали редагування раніше;) Так чи інакше ... як варіант, ви можете просто не заважати
чистити

Відповіді:


92

Думаючи тут нестандартно, як щодо власного 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)

28

Дотримуйтесь поточного коду. Це просто, і ви можете точно сказати що він робить, просто подивившись на це.

Інший спосіб - вбудований if:

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

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

Але це не набагато коротше, ніж у вас, і виглядає, можливо, гірше.

Ви також можете зробити sys.stdoutзакритим, але це не здається надто пітонічним:

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всередині цього менеджера контекстів і повернувши його до старого значення згодом. Але це здається трохи надто
задуманим

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/ asblock рівномірно, коли вам потрібно змусити його працювати заплутано? Ви додасте більше рядків і знизите продуктивність.


3
Винятки не слід використовувати для контролю "нормального" потоку процедури. Продуктивність? помилка буде швидше, ніж if / else?
Якуб М.

2
Залежить від ймовірності того, що ви будете користуватися тим чи іншим.
2rs2ts

31
@JakubM. Винятки можуть, повинні бути і використовуються таким чином у Python.
Gareth Latty

13
Беручи до уваги, що forцикл Python виходить, вловлюючи помилку StopIteration, викинуту ітератором, по якому він зациклюється, я б сказав, що використання винятків для управління потоком є ​​цілком пітонічним.
Kirk Strauser

1
Якщо припустити, що targetсаме Noneтоді призначений sys.stdout, вам потрібно ловити, TypeErrorа не IOError.
torek

5

Інше можливе рішення: не намагайтеся уникати методу виходу з менеджера контексту, просто продублюйте stdout.

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 проти 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

Це рішення не закриває файл явно при нормальному або помилковому припиненні пропозиції з, тому його не надто багато в контекстному менеджері. Клас, який реалізує вхід і вихід, був би кращим вибором.
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 - Це ... умовно. Обчислення кількості файлів у об'єкті дорівнює нулю, оскільки на нього не вказують змінних, тому його метод __del__ можна викликати негайно (у cpython) або пізніше, коли відбувається збір сміття. У документі є попередження не вірити, що це завжди буде працювати, але я весь час використовую його у коротших сценаріях. Щось велике, що працює довгий час і відкриває безліч файлів ... ну, я гадаю, я б використав 'with' або 'try / нарешті'.
tdelaney

ТІЛ. Я не знав, що файлові об'єкти " __del__будуть це робити".
2rs2ts

@ 2rs2ts: CPython використовує збирач сміття для підрахунку посилань (із "справжнім" GC, що викликається в міру необхідності), щоб він міг закрити файл, як тільки ви скинете всі посилання на дескриптор потоку. Jython і, мабуть, IronPython мають лише "справжній" GC, тому вони не закривають файл до можливого GC.
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 для stdout - це не те, що очікується. Наприклад, цей код усіче stdout, тим самим роблячи речі оболонки, такі як ./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.