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

У меня есть код, в котором я пытаюсь получить доступ к ресурсу, но иногда он недоступен и приводит к исключению. Я попытался реализовать механизм повторных попыток с помощью менеджеров контекста, но не могу обработать исключение, вызванное вызывающей стороной внутри контекста __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'

Есть ли способ перехватить эти исключения и обработать их внутри менеджера контекста?


person Mauro Baraldi    schedule 18.02.2016    source источник


Ответы (3)


Цитируя __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()
person thefourtheye    schedule 18.02.2016
comment
Но я ожидал, что он вернет попытки 3, а не 1 - person Mauro Baraldi; 18.02.2016
comment
@MauroBaraldi Это невозможно с менеджерами контекста. Возможно, вы захотите использовать декоратор для этого. - person thefourtheye; 18.02.2016
comment
@MauroBaraldi Я включил пример программы, которая уходит на пенсию. ПТАЛ. - person thefourtheye; 18.02.2016
comment
Спасибо за помощь, но мне нужно использовать его только в небольшой части кода, а не метод. В конце концов, это обходной путь для исправления другого (более уродливого) обходного пути. :-( - person Mauro Baraldi; 18.02.2016
comment
@MauroBaraldi Что ж, менеджеры контекста, как следует из названия, просто управляют контекстом для определенного блока кода. Они не контролируют поток управления. Таким образом, декоратор - единственный жизнеспособный/менее уродливый вариант. - person thefourtheye; 18.02.2016

Чтобы справиться с исключением в методе __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__, больше не существует - если ошибка возникает внутри блока 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:
        ...
person jsbueno    schedule 18.02.2016

Я думаю, что это легко, и другие люди, кажется, слишком много думают об этом. Просто поместите код получения ресурса в __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
person gil    schedule 18.02.2016