【问题标题】:Generator recovery using decorator使用装饰器恢复生成器
【发布时间】:2014-12-26 15:42:45
【问题描述】:

让我们有一个类,它的功能有时会失败,但经过一些操作后它就可以正常工作。

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

我已经尝试为此编写装饰器:

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)

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

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

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

        cursor.close()

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

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

我已经设法通过替换:

return func(self, *args, **kwargs)

与:

for row in func(self, *args, **kwargs):
    yield row

但我很好奇是否有更优雅(更pythonic)的方式来做到这一点。 有没有办法让 python 运行所有代码到第一个 yield 然后然后等待?

我知道只调用return tuple(func(self, *args, **kwargs)) 的可能性,但我想避免一次加载所有记录。

【问题讨论】:

    标签: python python-3.x decorator yield


    【解决方案1】:

    首先,我认为您当前使用的解决方案很好。当你装饰一个生成器时,装饰器至少需要表现得像一个在该生成器上的迭代器。通过使装饰器也成为生成器来做到这一点是完全可以的。正如 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
    

    您还可以让装饰器同时使用生成器和非生成器函数,使用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
    

    我更喜欢基于 yield/yield from 的解决方案,除非您需要在生成器之外装饰常规函数。

    【讨论】:

    • 我喜欢这个答案(+1-ed),但是当next(gen) 引发StopIteration 时,情况如何?
    • @Vyktor 我已经编辑了我的答案来处理这种情况。您可以捕获该异常并返回生成器对象,如果尝试对其进行迭代,则生成器对象将无操作(或者如果在其上调用next,则再次引发StopIteration)。
    • 老实说,这就是我建议在get_data 的循环之前添加另一个yield 的原因。不过,dano 的解决方案要清楚得多。
    • @x3al 我真的不喜欢编辑装饰函数以使其与装饰器正常工作的想法。因为如果你出于某种原因移除了装饰器,get_data 现在就坏了。对于阅读 get_data 而没有阅读装饰器的人来说,这看起来也很奇怪。
    【解决方案2】:

    有没有办法让python运行所有代码,直到第一次yield然后等待?

    是的,它被称为next(your_generator)。调用next() 一次,代码将在第一个yield 之后等待。如果您不想丢失第一个值,可以在循环之前放置另一个 yield

    如果你使用的是python 3.3+,也可以替换

    for row in func(self, *args, **kwargs):
        yield row
    

    yield from func(self, *args, **kwargs).

    【讨论】:

    • 你应该使用next(your_generator)obj.next()已在Python 3.x中被移除
    猜你喜欢
    • 2022-01-09
    • 1970-01-01
    • 2016-06-03
    • 1970-01-01
    • 2023-03-25
    • 2010-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多