有没有一种蟒蛇式的方法可以尝试最多次数的东西?

我有一个 python 脚本,它在一个共享的 linux 主机上查询 MySQL 服务器。出于某种原因,对 MySQL 的查询经常返回“ server has gone away”错误:

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

显然,我可以通过另一个尝试来实现这个目标,但是这个方法难以置信的丑陋,而且我有一种感觉,一定有一个体面的方法来实现这个目标。

71258 次浏览

How about:

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

I'd refactor it like so:

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

Factoring out the callee function seems to break up the functionality so that it's easy to see the business logic without getting bogged down in the retry code.

Building on Dana's answer, you might want to do this as a decorator:

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

Then...

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

Enhanced version that uses the decorator module

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

Then...

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

To install the decorator module:

$ easy_install decorator
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])

Like S.Lott, I like a flag to check if we're done:

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

This is my generic solution:

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

This allows code like the following:

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

Also possible:

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

This can be improved by handling exceptions in a more intuitive way, I hope. Open to suggestions.

def successful_transaction(transaction):
try:
transaction()
return True
except SQL...:
return False


succeeded = any(successful_transaction(transaction)
for transaction in repeat(transaction, 3))

UPDATE: there is a better maintained fork of the retrying library called tenacity, which supports more features and is in general more flexible.

The API changes slightly:

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


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


@retry(wait=wait_exponential(multiplier=1, min=4, max=10))
def wait_exponential_1000():
print("Wait 2^x * 1000 milliseconds between each retry,")
print("up to 10 seconds, then 10 seconds afterwards")

Yes, there is the retrying library, which has a decorator that implements several kinds of retrying logic that you can combine:

Some examples:

@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")

1.Definition:

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

I use it for parse html context.

You can use a for loop with an else clause for maximum effect:

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

The key is to break out of the loop as soon as the query succeeds. The else clause will only be triggered if the loop completes without a break.