czy istnieje pythonowy sposób na wypróbowanie czegoś do maksymalnej liczby razy? [duplikować]

86

Mam skrypt w Pythonie, który wysyła zapytanie do serwera MySQL na współużytkowanym hoście Linux. Z jakiegoś powodu zapytania do MySQL często zwracają błąd „serwer zniknął”:

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

Jeśli spróbujesz wykonać zapytanie ponownie natychmiast później, zwykle kończy się to pomyślnie. Tak więc chciałbym wiedzieć, czy w Pythonie istnieje rozsądny sposób, aby spróbować wykonać zapytanie, a jeśli się nie powiedzie, spróbować ponownie, do ustalonej liczby prób. Prawdopodobnie chciałbym spróbować 5 razy, zanim całkowicie się poddam.

Oto rodzaj kodu, który mam:

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])

Oczywiście mógłbym to zrobić, podejmując kolejną próbę w klauzuli oprócz, ale to jest niesamowicie brzydkie i mam wrażenie, że musi istnieć przyzwoity sposób na osiągnięcie tego.

Ben
źródło
2
Trafne spostrzeżenie. Prawdopodobnie prześpiłbym się na kilka sekund. Nie wiem, co jest nie tak z instalacją MySQL na serwerze, ale wydaje się, że w jednej sekundzie kończy się niepowodzeniem, a w następnej działa.
Ben
3
@Yuval O: To typowe zadanie. Podejrzewam, że jest nawet wbudowany w Erlang.
jfs
1
Wystarczy wspomnieć, że może nic się nie dzieje, Mysql ma zmienną wait_timeout , aby skonfigurować mysql tak, aby porzucał nieaktywne połączenia.
andy

Odpowiedzi:

98

Co powiesz na:

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
źródło
20
Lubfor attempt_number in range(3)
cdleary
8
Cóż, trochę mi się podoba, ponieważ wyraźnie wskazuje, że próby są zwiększane tylko w przypadku wyjątku.
Dana
2
Tak, wydaje mi się, że jestem bardziej paranoikiem, jeśli chodzi o wkradanie się nieskończonych whilepętli niż większość ludzi.
cdleary
5
-1: Nie lubię przerwy. Na przykład „jeszcze nie zrobione i próby <3:” lepiej.
S.Lott,
5
Lubię przerwy, ale nie chwilę. To bardziej przypomina C-ish niż pythonic. bo ja w zasięgu jest lepiej imho.
hasen
78

Opierając się na odpowiedzi Dany, możesz zrobić to jako dekorator:

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

Następnie...

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

Ulepszona wersja korzystająca z decoratormodułu

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

Następnie...

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

Aby zainstalować ten decoratormoduł :

$ easy_install decorator
dwc
źródło
2
Dekorator powinien prawdopodobnie również przyjąć klasę wyjątków, więc nie musisz używać gołego wyjątkiem; ie @retry (5, MySQLdb.Error)
cdleary
Świetnie! Nigdy nie myślę o używaniu dekoratorów: P
Dana,
To powinno być „return func () w bloku try, a nie tylko„ func () ”.
Robert Rossney,
Bah! Dzięki za ostrzeżenie.
dwc
Czy faktycznie próbowałeś to uruchomić? To nie działa. Problem polega na tym, że wywołanie func () w funkcji tryIt jest wykonywane, gdy tylko udekorujesz funkcję, a nie gdy faktycznie wywołasz funkcję dekorowaną. Potrzebujesz innej funkcji zagnieżdżonej.
Steve Losh
12

AKTUALIZACJA: istnieje lepiej zarządzana rozwidlenie biblioteki ponawiania zwane wytrwałością , która obsługuje więcej funkcji i jest ogólnie bardziej elastyczna.


Tak, istnieje biblioteka ponawiania , która ma dekorator, który implementuje kilka rodzajów logiki ponawiania, które można łączyć:

Kilka przykładów:

@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
źródło
2
Biblioteka ponownych prób została zastąpiona przez bibliotekę wytrwałości .
Seth
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
źródło
1
Możesz dodać jeszcze na dole:else: raise TooManyRetriesCustomException
Bob Stein
6

Zrobiłbym to tak:

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

Rozłożenie calleefunkcji na czynniki wydaje się przerywać funkcjonalność, dzięki czemu można łatwo zobaczyć logikę biznesową bez ugrzęźnięcia w kodzie ponawiania.

cdleary
źródło
-1: else i break ... okropnie. Preferuj jaśniejsze „jeszcze nie skończone i licz! = Próba_liczenia” niż przerwa.
S.Lott,
1
Naprawdę? Pomyślałem, że w ten sposób ma to większy sens - jeśli wyjątek nie wystąpi, wyrwij się z pętli. Być może nadmiernie boję się nieskończonych pętli while.
cdleary
4
+1: Nienawidzę zmiennych flag, gdy język zawiera struktury kodu, które robią to za Ciebie. Aby uzyskać punkty bonusowe, postaw inny na for, aby poradzić sobie z niepowodzeniem wszystkich prób.
xorsyst
6

Podobnie jak S.Lott, lubię flagę, aby sprawdzić, czy skończyliśmy:

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
źródło
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
źródło
1

1. definicja:

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

2. zastosowanie:

try_three_times(lambda: do_some_function_or_express())

Używam go do analizy kontekstu html.

user5637641
źródło
0

Oto moje ogólne rozwiązanie:

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

Pozwala to na kod podobny do następującego:

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

Również możliwe:

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

Mam nadzieję, że można to poprawić, obsługując wyjątki w bardziej intuicyjny sposób. Otwarci na sugestie.

user1970198
źródło
0

Możesz użyć forpętli z elseklauzulą ​​dla maksymalnego efektu:

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

Kluczem jest wyjście z pętli, gdy tylko zapytanie się powiedzie. elseKlauzula zostanie uruchomiona tylko wtedy, gdy pętla kończy bez break.

Szalony Fizyk
źródło