есть ли питонический способ попробовать что-то максимальное количество раз? [дубликат]

86

У меня есть сценарий python, который запрашивает сервер MySQL на общем хосте Linux. По какой-то причине запросы к MySQL часто возвращают ошибку «сервер ушел»:

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

Если сразу после этого попытаться выполнить запрос еще раз, обычно это удается. Итак, я хотел бы знать, есть ли в python разумный способ попытаться выполнить запрос, и, если он не удастся, повторить попытку до фиксированного количества попыток. Наверное, я хотел бы попробовать 5 раз, прежде чем совсем отказаться.

Вот какой у меня код:

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

Ясно, что я мог бы сделать это, выполнив еще одну попытку в разделе except, но это невероятно некрасиво, и я чувствую, что должен быть достойный способ добиться этого.

Бен
источник
2
Неплохо подмечено. Я бы, наверное, поспал на несколько секунд. Я не знаю, что не так с установкой MySQL на сервере, но кажется, что в одну секунду она не работает, а в следующую работает.
Ben
3
@Yuval A: Это обычная задача. Я подозреваю, что он даже встроен в Erlang.
jfs
1
Просто упомяну, что, возможно, все в порядке, Mysql имеет переменную wait_timeout для настройки mysql на удаление неактивных соединений.
Энди

Ответы:

98

Как насчет:

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])
Дана
источник
20
Or for attempt_number in range(3)
cdleary
8
Что ж, мне нравится мой, потому что он явно указывает, что попытки увеличиваются только в случае исключения.
Дана
2
Да, я полагаю, что я больше параноидально отношусь к появлению бесконечных whileциклов, чем большинство людей.
cdleary
5
-1: Не люблю перерыв. Типа «пока не сделано и попытки <3:» лучше.
S.Lott
5
Мне нравится перерыв, но не на время. Это больше похоже на C-ish, чем на pythonic. для i в диапазоне лучше имхо.
hasen
78

Основываясь на ответе Даны, вы можете сделать это как декоратор:

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

Потом...

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

Расширенная версия, использующая decoratorмодуль

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

Потом...

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

Для установки в decoratorмодуль :

$ easy_install decorator
dwc
источник
2
Декоратор, вероятно, также должен принимать класс исключения, поэтому вам не нужно использовать пустой except; ie @retry (5, MySQLdb.Error)
cdleary
Отлично! Я никогда не думаю об использовании декораторов: P
Дана
Это должно быть «return func () в блоке try, а не просто« func () ».
Роберт Россни
Ба! Спасибо за внимание.
dwc
Вы действительно пробовали это запустить? Не работает. Проблема в том, что вызов func () в функции tryIt выполняется, как только вы украшаете функцию, а не когда вы фактически вызываете декорированную функцию. Вам нужна еще одна вложенная функция.
Стив Лош,
12

ОБНОВЛЕНИЕ: существует улучшенная поддерживаемая ветвь библиотеки повторных попыток, называемая упорством , которая поддерживает больше функций и в целом более гибкая.


Да, есть библиотека повторных попыток , в которой есть декоратор, реализующий несколько видов логики повторных попыток, которые вы можете комбинировать:

Несколько примеров:

@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"
Элиас Дорнелес
источник
2
Библиотека повторных попыток была заменена библиотекой упорства .
Сет,
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])
веб-наркоман
источник
1
Вы можете добавить еще внизу:else: raise TooManyRetriesCustomException
Боб Стейн,
6

Я бы реорганизовал его так:

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

Вынесение calleeфункции за скобки, по- видимому, нарушает функциональность, так что бизнес-логику легко увидеть, не увязнув в коде повтора.

cdleary
источник
-1: иначе и сломать ... неприглядно. Предпочитайте более четкое выражение «пока еще не сделано и посчитайте! = Попытка_счета», чем перерыв.
S.Lott
1
В самом деле? Я думал, что в этом есть больше смысла - если исключение не возникает, выйдите из цикла. Я могу слишком бояться бесконечных циклов while.
cdleary
4
+1: я ненавижу флаговые переменные, когда язык включает структуры кода, которые делают это за вас. Чтобы получить бонусные баллы, поставьте еще на для того, чтобы справиться с провалом всех попыток.
xorsyst 05
6

Как и S.Lott, я люблю флаг, чтобы проверить, закончили ли мы:

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
Кив
источник
1
def successful_transaction(transaction):
    try:
        transaction()
        return True
    except SQL...:
        return False

succeeded = any(successful_transaction(transaction)
                for transaction in repeat(transaction, 3))
Питер Вуд
источник
1

1. определение:

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

2. Использование:

try_three_times(lambda: do_some_function_or_express())

Я использую его для разбора контекста html.

user5637641
источник
0

Это мое общее решение:

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

Это позволяет использовать следующий код:

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

Также возможно:

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

Надеюсь, это можно улучшить, обрабатывая исключения более интуитивно понятным способом. Открыт для предложений.

user1970198
источник
0

Для максимального эффекта можно использовать forцикл с elseпредложением:

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

Главное - выйти из цикла, как только запрос завершится успешно. Предложение elseбудет запущено только в том случае, если цикл завершится без файла break.

Безумный физик
источник