Обработка исключений в методе __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 шт):
Скорее тут подошло бы что-то такое:
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 вызываться. Нужно будет все конкретные исключения продумывать и обрабатывать по-разному.
Обработка ошибки внутри самого метода __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)