Обработка исключений внутри контекстных менеджеров
у меня есть код, который я пытаюсь достичь ресурса, но несколько раз он недоступен, и результаты в исключение. Я попытался реализовать механизм повтора с помощью контекст-менеджеров, но я не могу справиться с исключением, вызванным вызывающим абонентом внутри __enter__
context form context manager.
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'
есть ли способ перехватить это исключение(Ы) и обработать это внутри context manager?
3 ответов
цитирую __exit__
,
если предоставлено исключение, и метод хочет подавить исключение (т. е. предотвратить его распространение), он должен вернуть значение true. В противном случае исключение будет обработано нормально при выходе из этого метода.
по умолчанию, если вы не возвращаете значение явно из функции, Python вернет None
, что является ложным значением. В твоем случае, __exit__
возвращает None
и именно поэтому exeception разрешено протекать мимо __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()
иметь дело с исключением в __enter__
метод, самая простая (и менее удивительная) вещь, которую нужно сделать, - это обернуть with
сам оператор в предложении try-except и просто поднимает исключение -
а, with
блоки определенно не предназначены для такой работы - быть сами по себе" перезапускаемыми " - и здесь есть некоторое недоразумение:
def __enter__(self):
for _ in range(self.retries):
try:
self.attempts += 1
return self
except Exception as e:
err = e
после возвращения self
там контекст был __enter__
runs больше не существует - если ошибка происходит внутри 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:
...
Я думаю, что это легко, и другие люди, кажется, слишком много думаю. Просто поместите код извлечения ресурса в __enter__
, и попробуйте вернуться, а не self
, но ресурс извлечена. В коде:
def __init__(self, retries):
...
# for demo, let's 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