Как оставить блок with без закрытия ресурса?

avatar
Vogelsgesang
8 августа 2021 в 16:44
81
3
3

Я пытаюсь добиться чего-то похожего на

from tempfile import TemporaryFile

def open_head(file_path):
   with open(file_path, 'r') as f,
        TemporaryFile() as tf:
       for i in range(0,10):
           tf.write(f.read_line())
       return tf

таким образом, что вызывающий объект получает право собственности на временный файл.

В частности, я не хочу, чтобы оператор with закрывал TemporaryFile. Но если что-то пойдет не так до return, я все равно хочу, чтобы TemporaryFile закрывался оператором with.

В идеале я хотел бы записать вызывающего абонента как

with open_head(file_path):
    # more code here

Возможно ли это как-то? Например. написав return do_not_close(tf) или какую-то другую утилиту?

Или я подхожу к этому совершенно неправильно, и есть более Pythonic способ вернуть TemporaryFiles или другие ресурсы между функциями, гарантируя безопасность исключений?

Источник
Mark Ransom
8 августа 2021 в 17:00
3

Весь смысл оператора with в том, чтобы автоматически закрыть файл. Если это не то, что вы хотите, не используйте его.

Ответы (3)

avatar
chepner
8 августа 2021 в 17:01
7

Нет. open_head должен принимать уже открытый дескриптор, за закрытие которого отвечает вызывающий объект.

from tempfile import TemporaryFile
from itertools import islice


def head(file_path, fh):
    with open(file_path) as f:
        for line in islice(f, 10):
            fh.write(line)


with TemporaryFile() as tf:
    head(file_path, tf)
    # Do other stuff with tf before it gets closed.
    

Вообще, всякий раз, когда вы открываете файл в функции, спросите себя, можете ли вы передать фактическое открытие вызывающему объекту и вместо этого принять файлоподобный объект. Помимо того, что ваш код можно использовать повторно, это упрощает его тестирование. head не обязательно вызывать с реальным файлом: его можно вызывать с любым файлоподобным объектом, например io.StringIO.


Иными словами: оператор with применяет совет

Если вы открываете файл, вы также несете ответственность за его закрытие.

противоположный этого совета

Если вы не несете ответственности за закрытие файла, вы также не несете ответственности за его открытие.

avatar
Yu Chen
8 августа 2021 в 16:58
0

Возможно, я неправильно понял ваш вопрос, но не могли бы вы явным образом открыть временный файл, вернуть его из функции, а затем закрыть, когда захотите?

def open_head(...):
   with open(file_path, 'r') as f:
      tf = TemporaryFile()
      for i in range(0,10):
         tf.write(f.read_line())
   return tf

tf = open_head(...)

# do some work

tf.close() # close the temporary file

Убедитесь, что вы понимаете: возвращая tf из функции open_head, вы сами несете ответственность за ее правильное закрытие.

Ключевое слово with используется с менеджерами контекста, которые имеют методы __enter__ и __exit__. Метод __exit__ вызывается при выходе из блока with, который закрывает дескриптор файла.

Если вы хотите обрабатывать исключения, вы можете предоставить дескриптор временного файла

@contextlib.contextmanager
def open_head(...):
    tf = TemporaryFile()

    try:
        # do some work that might cause errors
        yield tf
    finally:
        os.close(tf)
Vogelsgesang
8 августа 2021 в 17:01
0

Да, это то, чем я сейчас занимаюсь. Проблема с этим решением заключается в том, что если, например. f.read_line() вызывает исключение, TemporaryFile будет просочено. Я ищу безопасный способ исключения, желательно без использования try-catch

Yu Chen
8 августа 2021 в 17:06
0

@Vogelsgesang, вы уверены, что вам нужен try catch для этого? В документации для TemporaryFile (docs.python.org/3/library/tempfile.html#tempfile.TemporaryFile) говорится: «Он будет уничтожен, как только будет закрыт (включая неявное закрытие, когда объект вывоз мусора).". Таким образом, если возникает исключение, выполнение функции прерывается, локальная переменная tf выходит из области видимости, собирается мусор и неявно закрывается.

Yu Chen
8 августа 2021 в 17:32
0

@Vogelsgesang см. этот пример — gist.github.com/ychennay/319e14f35becb93b556e6637c7f5ce8b. Временный файл закрывается, как только он выходит за рамки. Когда вы возвращаете его из функции open_head, вы сами несете ответственность за его закрытие.

avatar
Jacob
8 августа 2021 в 16:56
1

Просто переместите TemporaryFile за пределы менеджера контекста и поместите его в блок try except

from tempfile import TemporaryFile

def open_head(path: str):
    try:
        tf = TemporaryFile()
        with open(path, "r") as f:
            for _ in range(10):
                tf.write(f.readline())
            return tf
    except Exception as e:
        tf.close()
        raise e
Charles Duffy
8 августа 2021 в 17:00
4

Может потребоваться raise в этом блоке except, чтобы мы не отбрасывали исключения.

chepner
8 августа 2021 в 17:18
1

Но теперь вы потеряли гарантию того, что каждый tf будет правильно закрыт. Вы открыли его, но вы не знаете, кто, если вообще кто-нибудь, закроет его.