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?
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()
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:
...
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