web-dev-qa-db-fra.com

Gestion des exceptions dans les gestionnaires de contexte

J'ai du code où j'essaie d'accéder à une ressource mais parfois il n'est pas disponible et entraîne une exception. J'ai essayé d'implémenter un moteur de nouvelle tentative en utilisant gestionnaires de contexte, mais je ne peux pas gérer l'exception déclenchée par l'appelant dans le __enter__ contexte pour mon gestionnaire de contexte.

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

Ce sont quelques exemples qui lèvent juste une exception (que je m'attendais à gérer)

>>> 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'

Existe-t-il un moyen d'intercepter ces exceptions et de les gérer dans le gestionnaire de contexte?

18
Mauro Baraldi

Citant __exit__ ,

Si une exception est fournie et que la méthode souhaite supprimer l'exception (c'est-à-dire l'empêcher de se propager), elle doit renvoyer une valeur vraie . Sinon, l'exception sera traitée normalement à la sortie de cette méthode.

Par défaut, si vous ne renvoyez pas une valeur explicitement à partir d'une fonction, Python renverra None, qui est une valeur fausse. Dans votre cas, __exit__ renvoie None et c'est pourquoi l'exception est autorisée à s'écouler au-delà du __exit__.

Donc, retournez une valeur vraie, comme ceci

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

la sortie sera

Attempts 1
<type 'exceptions.NameError'> name 'ok' is not defined

Si vous voulez avoir la fonctionnalité de nouvelle tentative, vous pouvez l'implémenter avec un générateur, comme celui-ci

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()
22
thefourtheye

Pour traiter une exception dans une méthode __enter__, La chose la plus simple (et la moins surprenante) à faire serait d'encapsuler l'instruction with elle-même dans une clause try-except et de simplement augmenter l'éxéption -

Mais, les blocs with ne sont certainement pas conçus pour fonctionner comme ça - pour être, par eux-mêmes "récupérables" - et il y a un malentendu ici:

def __enter__(self):
    for _ in range(self.retries):
        try:
            self.attempts += 1
            return self
        except Exception as e:
            err = e

Une fois que vous y avez retourné self, le contexte dans lequel __enter__ S'exécute n'existe plus - si une erreur se produit à l'intérieur du bloc with, elle se déplacera naturellement vers le __exit__ méthode. Et non, la méthode __exit__ Ne peut en aucun cas faire remonter le flux d'exécution au début du bloc with.

Vous voulez probablement quelque chose de plus comme ceci:

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:
        ...
8
jsbueno

Je pense que celui-ci est facile, et d'autres personnes semblent y penser. Mettez simplement le code de récupération des ressources dans __enter__, et essayez de retourner, pas self, mais la ressource récupérée. Dans du code:

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

Essayez-le maintenant:

>>> 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
4
gil