Обработка исключений в методе __enter__ менеджера контекста

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

class open_json:
    def __init__(self, file_path):
        self.file_path = file_path

    def __enter__(self):
        print('start enter')
        temp_file = open(self.file_path, 'r')
        return json.load(temp_file)

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('start exit')
        os.unlink(self.file_path)

with open_json(r'<path to>.json') as res:
    tree = [res]

Однако на этапе тестирования, когда я подсовываю json с заведомо недесериализуемым содержимым, то в __enter__ возникает ошибка и __exit__ не выполняется. Не могу понять почему. А мне в любом случае надо удалить файл, и, если возникла ошибка, то вывести ее как Exception


Ответы (2 шт):

Автор решения: CrazyElf

Скорее тут подошло бы что-то такое:

def open_json(file_path):
    temp_file = open(file_path)
    try:
        return json.load(temp_file)
    except:
        pass
    finally:
        temp_file.close()
        os.unlink(file_path)

res = open_json(r'<path to>.json')
tree = [res]

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

  • когда падает эксепшен при открытии файла
  • когда падает эксепшен при чтении json

В приведённом мной вариаенте эксепшен на битом json будет игнорирован, а вот проблема с открытием файла будет эскалирована. Но возможны и другие варианты обработки. Вам главное определиться какое вы хотите поведение метода/класса.

Если вы знаете конкретное исключение, которое хотите игнорировать, то можно сделать чуть более красиво:

def open_json(file_path):
    try:
        with open(file_path) temp_file:
            return json.load(temp_file)
    except ИсключениеJsonКотороеНужноИгнорить:
        pass
    finally:
        os.unlink(file_path)

P.S. Хотя нет, в этом случае опять же нужно будет что-то делать со случаями, когда к файлу нет доступа и т.д., тут лишний раз будет ещё unlink вызываться. Нужно будет все конкретные исключения продумывать и обрабатывать по-разному.

→ Ссылка
Автор решения: insolor

Обработка ошибки внутри самого метода __enter__ сама по себе не происходит:

class open_json:
    def __init__(self, file_path):
        self.file_path = file_path

    def __enter__(self):
        print('start enter')
        raise RuntimeError("test")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('start exit')


with manager(r'<path to>.json') as res:
    pass

Вывод:

start enter
Traceback (most recent call last):
  File "/home/user/Projects/test.py", line 13, in <module>
    with manager(r'<path to>.json') as res:
  File "/home/user/Projects/test.py", line 7, in __enter__
    raise RuntimeError("test")
RuntimeError: test

Вывода "start exit" нет, значит __exit__ не сработал.

Хотя при вылете ошибки внутри блока with метод __exit__ срабатывает:

class manager:
    def __init__(self, file_path):
        self.file_path = file_path

    def __enter__(self):
        print('start enter')
        # raise RuntimeError("test")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('start exit')


with manager(r'<path to>.json') as res:
    raise RuntimeError("test")

Вывод:

start enter
start exit
Traceback (most recent call last):
  File "/home/user/Projects/test.py", line 14, in <module>
    raise RuntimeError("test")
RuntimeError: test

Поэтому внутри __enter__ ошибку нужно обработать вручную: обернуть код в try-except, в except вызвать метод __exit__, потом пробросить ошибку наружу:

class manager:
    def __init__(self, file_path):
        self.file_path = file_path

    def __enter__(self):
        print('start enter')
        try:
            raise RuntimeError("test")
        except Exception as ex:
            self.__exit__(type(ex), ex, ex.__traceback__)
            raise ex

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('start exit')


with manager(r'<path to>.json') as res:
    pass

Вывод:

start enter
start exit
Traceback (most recent call last):
  File "/home/user/Projects/test.py", line 17, in <module>
    with manager(r'<path to>.json') as res:
  File "/home/user/Projects/test.py", line 11, in __enter__
    raise ex
  File "/home/user/Projects/test.py", line 8, in __enter__
    raise RuntimeError("test")
RuntimeError: test

Удобнее реализовывать контекстные менеджеры через функции с декоратором contextmanager.

Внутри функции весь код, который потенциально может вызвать ошибку (в том числе и передача данных наружу), нужно обернуть в try-finally. В finally нужно добавить тот код, который у вас должен выполняться при выходе из блока with:

from contextlib import contextmanager


@contextmanager
def manager(file_path):
    try:
        print('start enter')
        raise RuntimeError("test")
        yield
    finally:
        print('start exit')


with manager(r'<path to>.json') as res:
    pass

Вывод:

start enter
start exit
Traceback (most recent call last):
  File "/home/user/Projects/test.py", line 13, in <module>
    with manager(r'<path to>.json') as res:
  File "/usr/lib/python3.10/contextlib.py", line 281, in helper
    return _GeneratorContextManager(func, args, kwds)
  File "/usr/lib/python3.10/contextlib.py", line 103, in __init__
    self.gen = func(*args, **kwds)
  File "/home/user/Projects/test.py", line 8, in manager
    raise RuntimeError("test")
RuntimeError: test

При возникновении ошибки сначала будет выполняться блок finally, потом уже ошибка ошибка пробросится наружу, т.к. у нас except нет. Дальше уже снаружи блока with можно будет добавить обработку ошибки, но в данном случае главное, что несмотря на ошибку финальные действия у нас выполнились.

На вашем примере будет что-то такое:

import json
import os
from contextlib import contextmanager


@contextmanager
def manager(file_path):
    try:
        print('start enter')
        with open(file_path, 'r') as temp_file:
            yield json.load(temp_file)
    finally:
        print('start exit')
        os.unlink(file_path)
→ Ссылка