Recuperación del generador mediante decorador.

Tengamos una clase que tenga una función que falle de vez en cuando, pero después de algunas acciones simplemente funciona perfectamente.

El ejemplo de la vida real sería Mysql Query, que genera _mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away') pero después de la reconexión del cliente funciona bien.

He intentado escribir decorador para esto:

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

Y si el cliente pierde la conexión, simplemente se vuelve a conectar y todos están contentos.

Pero qué get_data() si quiero transformar get_data() en generador (y usar la statement de yield ):

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

Bueno, el ejemplo anterior no funcionará porque la función interna ya devolvió el generador y se interrumpirá después de llamar primero a next() .

Como lo entiendo, si Python ve el yield dentro del método, simplemente cede el control de inmediato ( sin ejecutar una sola instrucción ) y espera a la primera next() .

Me las arreglé para hacerlo funcionar reemplazando:

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

Con:

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

Pero tengo curiosidad por saber si hay una forma más elegante (más python) de hacerlo. ¿Hay alguna manera de hacer que Python ejecute todo el código hasta el primer yield y luego espere?

Soy consciente de la posibilidad de simplemente llamar return tuple(func(self, *args, **kwargs)) pero quiero evitar cargar todos los registros a la vez.

Primero, creo que la solución que estás usando actualmente está bien. Cuando decoras un generador, el decorador necesitará al menos comportarse como un iterador sobre ese generador. Hacer eso al convertir al decorador en un generador, también, está perfectamente bien. Como señaló x3al, usar el yield from func(...) lugar de for row in func(...): yield row es una posible optimización.

Si también desea evitar que el decorador se convierta en un generador, puede hacerlo utilizando next , que se ejecutará hasta el primer yield , y devolverá el primer valor de rendimiento. Deberá hacer que el decorador de alguna manera capture y devuelva ese primer valor, además del rest de los valores que generará el generador. Podrías hacer eso con 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 

También puede hacer que el decorador trabaje con las funciones de generador y sin generador, utilizando inspect para determinar si está decorando un generador:

 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 

Yo preferiría la solución basada en yield / yield from , a menos que tenga el requisito de decorar las funciones regulares además de los generadores.

¿Hay alguna manera de hacer que Python ejecute todo el código hasta el primer rendimiento y luego espere?

Sí y se llama next(your_generator) . Llame a next() una vez y el código esperará exactamente después del primer yield . Puede colocar otro yield justo antes del bucle si no desea perder el primer valor.

Si está usando Python 3.3+, también puede reemplazar

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

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