y a-t-il un moyen pythonique d'essayer quelque chose jusqu'à un maximum de fois? [dupliquer]

85

J'ai un script python qui interroge un serveur MySQL sur un hôte Linux partagé. Pour une raison quelconque, les requêtes adressées à MySQL renvoient souvent une erreur "Le serveur est parti":

_mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away')

Si vous réessayez la requête immédiatement après, elle réussit généralement. Donc, j'aimerais savoir s'il existe un moyen raisonnable en python d'essayer d'exécuter une requête, et si cela échoue, de réessayer, jusqu'à un nombre fixe d'essais. Je voudrais probablement qu'il essaie 5 fois avant d'abandonner complètement.

Voici le type de code que j'ai:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

try:
    cursor.execute(query)
    rows = cursor.fetchall()
    for row in rows:
        # do something with the data
except MySQLdb.Error, e:
    print "MySQL Error %d: %s" % (e.args[0], e.args[1])

Il est clair que je pourrais le faire en faisant une autre tentative dans la clause d'exception, mais c'est incroyablement moche, et j'ai le sentiment qu'il doit y avoir un moyen décent d'y parvenir.

Ben
la source
2
C'est un bon point. J'aurais probablement dormi quelques secondes. Je ne sais pas ce qui ne va pas avec l'installation de MySQL sur le serveur, mais il semble que cela échoue une seconde, et la suivante, cela fonctionne.
Ben
3
@Yuval A: C'est une tâche courante. Je soupçonne qu'il est même intégré à Erlang.
jfs
1
Juste pour mentionner que rien ne va peut-être, Mysql a une variable wait_timeout pour configurer mysql afin de supprimer les connexions inactives.
andy

Réponses:

97

Que diriez-vous:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()
attempts = 0

while attempts < 3:
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
    except MySQLdb.Error, e:
        attempts += 1
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
Dana
la source
19
Oufor attempt_number in range(3)
cdleary
8
Eh bien, j'aime un peu la mienne car elle rend explicite que les tentatives ne sont augmentées qu'en cas d'exception.
Dana
2
Ouais, je suppose que je suis plus paranoïaque à propos des whileboucles infinies qui s'insinuent que la plupart des gens.
cdleary
5
-1: Je n'aime pas la pause. Comme "tant que ce n'est pas fait et tente <3:" mieux.
S.Lott
5
J'aime la pause, mais pas le temps. Cela ressemble plus à C-ish qu'à pythonique. pour i à portée est mieux à mon humble avis.
hasen le
78

En vous appuyant sur la réponse de Dana, vous voudrez peut-être faire ceci en tant que décorateur:

def retry(howmany):
    def tryIt(func):
        def f():
            attempts = 0
            while attempts < howmany:
                try:
                    return func()
                except:
                    attempts += 1
        return f
    return tryIt

Ensuite...

@retry(5)
def the_db_func():
    # [...]

Version améliorée qui utilise le decoratormodule

import decorator, time

def retry(howmany, *exception_types, **kwargs):
    timeout = kwargs.get('timeout', 0.0) # seconds
    @decorator.decorator
    def tryIt(func, *fargs, **fkwargs):
        for _ in xrange(howmany):
            try: return func(*fargs, **fkwargs)
            except exception_types or Exception:
                if timeout is not None: time.sleep(timeout)
    return tryIt

Ensuite...

@retry(5, MySQLdb.Error, timeout=0.5)
def the_db_func():
    # [...]

Pour installer le decoratormodule :

$ easy_install decorator
dwc
la source
2
Le décorateur devrait probablement aussi prendre une classe d'exception, vous n'avez donc pas à utiliser une exception nue; ie @retry (5, MySQLdb.Error)
cdleary
Chouette! Je ne pense jamais à utiliser des décorateurs: P
Dana
Cela devrait être "return func () dans le bloc try, pas seulement" func () ".
Robert Rossney
Bah! Merci pour l'information.
dwc
Avez-vous réellement essayé d'exécuter cela? Ça ne marche pas. Le problème est que l'appel func () dans la fonction tryIt est exécuté dès que vous décorez la fonction, et non lorsque vous appelez réellement la fonction décorée. Vous avez besoin d'une autre fonction imbriquée.
Steve Losh
12

MISE À JOUR: il existe un fork de la bibliothèque de relance mieux maintenu appelé ténacité , qui prend en charge plus de fonctionnalités et est en général plus flexible.


Oui, il existe la bibliothèque de relance , qui a un décorateur qui implémente plusieurs types de logique de relance que vous pouvez combiner:

Quelques exemples:

@retry(stop_max_attempt_number=7)
def stop_after_7_attempts():
    print "Stopping after 7 attempts"

@retry(wait_fixed=2000)
def wait_2_s():
    print "Wait 2 second between retries"

@retry(wait_exponential_multiplier=1000, wait_exponential_max=10000)
def wait_exponential_1000():
    print "Wait 2^x * 1000 milliseconds between each retry,"
    print "up to 10 seconds, then 10 seconds afterwards"
Elias Dorneles
la source
2
La bibliothèque de relance a été remplacée par la bibliothèque de ténacité .
Seth le
8
conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

for i in range(3):
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
webjunkie
la source
1
Vous pouvez en ajouter un autre en bas:else: raise TooManyRetriesCustomException
Bob Stein
6

Je le refactoriserais comme ceci:

def callee(cursor):
    cursor.execute(query)
    rows = cursor.fetchall()
    for row in rows:
        # do something with the data

def caller(attempt_count=3, wait_interval=20):
    """:param wait_interval: In seconds."""
    conn = MySQLdb.connect(host, user, password, database)
    cursor = conn.cursor()
    for attempt_number in range(attempt_count):
        try:
            callee(cursor)
        except MySQLdb.Error, e:
            logging.warn("MySQL Error %d: %s", e.args[0], e.args[1])
            time.sleep(wait_interval)
        else:
            break

La factorisation de la calleefonction semble briser la fonctionnalité afin qu'il soit facile de voir la logique métier sans s'enliser dans le code de nouvelle tentative.

cdleary
la source
-1: sinon et casser ... icky. Préférez un "while not done and count! = Try_count" plus clair que break.
S.Lott
1
Vraiment? Je pensais que cela avait plus de sens de cette façon - si l'exception ne se produit pas, sortez de la boucle. J'ai peut-être trop peur des boucles while infinies.
cdleary
4
+1: Je déteste les variables de drapeau lorsque le langage inclut les structures de code pour le faire pour vous. Pour les points bonus, mettez un autre sur le pour faire face à l'échec de toutes les tentatives.
xorsyst
6

Comme S.Lott, j'aime un drapeau pour vérifier si nous avons terminé:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

success = False
attempts = 0

while attempts < 3 and not success:
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        success = True 
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
        attempts += 1
Kiv
la source
1
def successful_transaction(transaction):
    try:
        transaction()
        return True
    except SQL...:
        return False

succeeded = any(successful_transaction(transaction)
                for transaction in repeat(transaction, 3))
Peter Wood
la source
1

1.Définition:

def try_three_times(express):
    att = 0
    while att < 3:
        try: return express()
        except: att += 1
    else: return u"FAILED"

2. utilisation:

try_three_times(lambda: do_some_function_or_express())

Je l'utilise pour analyser le contexte html.

user5637641
la source
0

Voici ma solution générique:

class TryTimes(object):
    ''' A context-managed coroutine that returns True until a number of tries have been reached. '''

    def __init__(self, times):
        ''' times: Number of retries before failing. '''
        self.times = times
        self.count = 0

    def __next__(self):
        ''' A generator expression that counts up to times. '''
        while self.count < self.times:
            self.count += 1
        yield False

    def __call__(self, *args, **kwargs):
        ''' This allows "o() calls for "o = TryTimes(3)". '''
        return self.__next__().next()

    def __enter__(self):
        ''' Context manager entry, bound to t in "with TryTimes(3) as t" '''
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        ''' Context manager exit. '''
        return False # don't suppress exception

Cela permet un code comme celui-ci:

with TryTimes(3) as t:
    while t():
        print "Your code to try several times"

Aussi possible:

t = TryTimes(3)
while t():
    print "Your code to try several times"

Cela peut être amélioré en gérant les exceptions de manière plus intuitive, j'espère. Ouvert aux suggestions.

user1970198
la source
0

Vous pouvez utiliser une forboucle avec une elseclause pour un effet maximal:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

for n in range(3):
    try:
        cursor.execute(query)
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
    else:
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
else:
    # All attempts failed, raise a real error or whatever

La clé est de sortir de la boucle dès que la requête aboutit. La elseclause ne sera déclenchée que si la boucle se termine sans un break.

Physicien fou
la source