Есть ли питонический способ попробовать что-то до максимального количества раз?
У меня есть python script, который запрашивает сервер 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, но это невероятно уродливо, и у меня есть ощущение, что для достижения этого должен быть достойный способ.
Ответы
Ответ 1
Как насчет:
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])
Ответ 2
На основе ответа Даны вы можете сделать это как декоратор:
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
Ответ 3
ОБНОВЛЕНИЕ: имеется улучшенная версия fork библиотеки повторного запуска, называемая tenacity, которая поддерживает больше функции и, в целом, более гибкие.
Да, есть повторная библиотека, в которой есть декоратор, который реализует несколько видов логики повторения, которые вы можете комбинировать:
Некоторые примеры:
@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"
Ответ 4
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])
Ответ 5
Как и 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
Ответ 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
, по-видимому, разрушает функциональность, поэтому легко видеть бизнес-логику, не увязнув в повторном коде.
Ответ 7
1. Определение:
def try_three_times(express):
att = 0
while att < 3:
try: return express()
except: att += 1
else: return u"FAILED"
2.Usage:
try_three_times(lambda: do_some_function_or_express())
Я использую его для контекста анализа html.
Ответ 8
Это мое общее решение:
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"
Это может быть улучшено путем обработки исключений более интуитивным способом, я надеюсь. Откройте для предложений.
Ответ 9
def successful_transaction(transaction):
try:
transaction()
return True
except SQL...:
return False
succeeded = any(successful_transaction(transaction)
for transaction in repeat(transaction, 3))