使用装饰器恢复发电机

Vyk*_*tor 8 python yield decorator python-3.x

让我们有一个具有不时失败的功能的类,但是在一些动作之后它才能完美地运行.

现实生活中的例子是Mysql Query提升,_mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away')但在客户端重新连接后它可以正常工作.

我试过写这个装饰器:

def _auto_reconnect_wrapper(func):
    ''' Tries to reconnects dead connection
    '''

    def inner(self, *args, _retry=True, **kwargs):
        try:
            return func(self, *args, **kwargs)

        except Mysql.My.OperationalError as e:
            # No retry? Rethrow
            if not _retry:
                raise

            # Handle server connection errors only
            # http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html
            if (e.code < 2000) or (e.code > 2055):
                raise

            # Reconnect
            self.connection.reconnect()

        # Retry
        return inner(self, *args, _retry=False, **kwargs)
    return inner

class A(object):
    ...

    @_auto_reconnect_wrapper
    def get_data(self):
        sql = '...'
        return self.connection.fetch_rows(sql)
Run Code Online (Sandbox Code Playgroud)

如果客户失去联系,它只是默默地重新连接,每个人都很高兴.

但是如果我想转换get_data()为生成器(和使用yield语句)怎么办:

    @_auto_reconnect_wrapper
    def get_data(self):
        sql = '...'
        cursor = self.connection.execute(sql)
        for row in cursor:
            yield row

        cursor.close()
Run Code Online (Sandbox Code Playgroud)

好吧,前面的例子不起作用,因为内部函数已经返回了生成器,并且在调用之后它将会中断next().

据我了解,如果python看到yield内部方法,它只是立即产生控制(不执行一个单一语句)并等待第一个next().

我已经成功通过替换:

return func(self, *args, **kwargs)
Run Code Online (Sandbox Code Playgroud)

附:

for row in func(self, *args, **kwargs):
    yield row
Run Code Online (Sandbox Code Playgroud)

但我很好奇是否有更优雅(更pythonic)的方式来做到这一点.有没有一种方法,使蟒蛇运行所有代码,使其先yield等待?

我知道只是打电话的可能性,return tuple(func(self, *args, **kwargs))但我想避免一次加载所有记录.

dan*_*ano 7

首先,我认为您目前使用的解决方案很好.当你装饰一个生成器时,装饰器至少需要像该生成器上的迭代器一样.通过使装饰器成为发电机来做到这一点也是完全可以的.正如x3al指出的那样,使用yield from func(...)而不是for row in func(...): yield row可能的优化.

如果你想避免实际上使装饰器成为一个生成器,你可以通过使用来实现next,它将运行到第一个yield,并返回第一个产生的值.除了要由生成器生成的其余值之外,您还需要使装饰器以某种方式捕获并返回第一个值.你可以这样做itertools.chain:

def _auto_reconnect_wrapper(func):
    ''' Tries to reconnects dead connection
    '''

    def inner(self, *args, _retry=True, **kwargs):
        gen = func(self, *args, **kwargs)
        try:
            value = next(gen)
            return itertools.chain([value], gen)
        except StopIteration:
            return gen
        except Mysql.My.OperationalError as e:
            ...
            # Retry
            return inner(self, *args, _retry=False, **kwargs)
    return inner
Run Code Online (Sandbox Code Playgroud)

您还可以使装饰器使用生成器和非生成器函数,inspect以确定您是否正在装饰生成器:

def _auto_reconnect_wrapper(func):
    ''' Tries to reconnects dead connection
    '''

    def inner(self, *args, _retry=True, **kwargs):
        try:
            gen = func(self, *args, **kwargs)
            if inspect.isgenerator(gen):
                value = next(gen)
                return itertools.chain([value], gen)
            else: # Normal function
                return gen
        except StopIteration:
            return gen
        except Mysql.My.OperationalError as e:
            ...
            # Retry
            return inner(self, *args, _retry=False, **kwargs)
    return inner
Run Code Online (Sandbox Code Playgroud)

我倾向于基于yield/ yield from的解决方案,除非您需要除了生成器之外还要装饰常规函数.