Обработка исключений внутри контекстных менеджеров
У меня есть некоторый код, где я пытаюсь получить доступ к ресурсу, но иногда он недоступен и приводит к исключению. Я попытался реализовать механизм повторных попыток, используя менеджеры контекста, но не могу обработать исключение, вызванное вызывающей стороной в контексте __enter__
для моего менеджера контекста.
class retry(object):
def __init__(self, retries=0):
self.retries = retries
self.attempts = 0
def __enter__(self):
for _ in range(self.retries):
try:
self.attempts += 1
return self
except Exception as e:
err = e
def __exit__(self, exc_type, exc_val, traceback):
print 'Attempts', self.attempts
Это несколько примеров, которые просто вызывают исключение (которое я ожидал обработать)
>>> with retry(retries=3):
... print ok
...
Attempts 1
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
NameError: name 'ok' is not defined
>>>
>>> with retry(retries=3):
... open('/file')
...
Attempts 1
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IOError: [Errno 2] No such file or directory: '/file'
Есть ли способ перехватить эти исключения и обработать их внутри диспетчера контекста?
Ответы
Ответ 1
Цитата __exit__
,
Если предоставляется исключение, и метод хочет подавить исключение (т.е. предотвратить его распространение), он должен вернуть истинное значение. В противном случае исключение будет обработано обычно после выхода из этого метода.
По умолчанию, если вы не возвращаете значение явно из функции, Python вернет None
, что является ложным значением. В вашем случае __exit__
возвращает None
, и поэтому исключение может проходить через __exit__
.
Итак, верните правдивое значение, например
class retry(object):
def __init__(self, retries=0):
...
def __enter__(self):
...
def __exit__(self, exc_type, exc_val, traceback):
print 'Attempts', self.attempts
print exc_type, exc_val
return True # or any truthy value
with retry(retries=3):
print ok
вывод будет
Attempts 1
<type 'exceptions.NameError'> name 'ok' is not defined
Если вы хотите иметь функцию повтора, вы можете реализовать это с помощью генератора, например,
def retry(retries=3):
left = {'retries': retries}
def decorator(f):
def inner(*args, **kwargs):
while left['retries']:
try:
return f(*args, **kwargs)
except NameError as e:
print e
left['retries'] -= 1
print "Retries Left", left['retries']
raise Exception("Retried {} times".format(retries))
return inner
return decorator
@retry(retries=3)
def func():
print ok
func()
Ответ 2
Чтобы справиться с исключением в методе __enter__
, наиболее простой (и менее удивительный) способ заключался бы в том, чтобы заключить сам оператор with
в предложение try-Кроме, и просто вызвать исключение -
Но блоки with
определенно не предназначены для такой работы, то есть сами по себе "повторяющиеся", и здесь есть некоторое недопонимание:
def __enter__(self):
for _ in range(self.retries):
try:
self.attempts += 1
return self
except Exception as e:
err = e
Как только вы вернете self
, контекст, в котором выполняется __enter__
, больше не существует - если в блоке with
произойдет ошибка, он просто будет естественным образом передан методу __exit__
. И нет, метод __exit__
никоим образом не может заставить поток выполнения вернуться к началу блока with
.
Вы, вероятно, хотите что-то более похожее на это:
class Retrier(object):
max_retries = 3
def __init__(self, ...):
self.retries = 0
self.acomplished = False
def __enter__(self):
return self
def __exit__(self, exc, value, traceback):
if not exc:
self.acomplished = True
return True
self.retries += 1
if self.retries >= self.max_retries:
return False
return True
....
x = Retrier()
while not x.acomplished:
with x:
...
Ответ 3
Я думаю, что это легко, и другие люди, похоже, переусердствовали. Просто поставьте код выбора ресурсов в __enter__
и попробуйте вернуться, а не self
, но извлеченный ресурс. В коде:
def __init__(self, retries):
...
# for demo, let add a list to store the exceptions caught as well
self.errors = []
def __enter__(self):
for _ in range(self.retries):
try:
return resource # replace this with real code
except Exception as e:
self.attempts += 1
self.errors.append(e)
# this needs to return True to suppress propagation, as others have said
def __exit__(self, exc_type, exc_val, traceback):
print 'Attempts', self.attempts
for e in self.errors:
print e # as demo, print them out for good measure!
return True
Теперь попробуйте:
>>> with retry(retries=3) as resource:
... # if resource is successfully fetched, you can access it as `resource`;
... # if fetching failed, `resource` will be None
... print 'I get', resource
I get None
Attempts 3
name 'resource' is not defined
name 'resource' is not defined
name 'resource' is not defined