2014-10-30 12 views
8

Lassen Sie uns eine Klasse haben, die Funktion hat, die von Zeit zu Zeit fehlschlägt, aber nach einigen Aktionen funktioniert es einfach perfekt.Generator Erholung mit Dekorator

Beispiel für ein echtes Leben wäre Mysql Abfrage, die _mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away') erhöht, aber nach Client Reconnection funktioniert es gut.

Ich habe versucht, Dekorateur für diese zu schreiben:

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) 

Und wenn Client-Verbindung es glücklich, nur leise wieder und jeder ist verliert.

Aber was, wenn ich get_data() zum Generator (und verwenden yield Anweisung) zu transformieren will:

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

     cursor.close() 

Nun, vorheriges Beispiel wird nicht funktionieren, weil innere Funktion bereits zurück Generator, und es bricht nach dem ersten Aufruf next() .

Wie ich es verstehen, wenn Python yield Innere Methode sieht sie die Steuerung nur unmittelbar ergibt (ohne eine einzige Anweisung ausführen) und wartet auf erste next().

Ich habe es geschafft, um es durch den Austausch funktioniert:

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

mit:

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

Aber ich bin gespannt, ob es elegantere (mehr pythonic) Weg, dies zu tun. Gibt es eine Möglichkeit, Python den gesamten Code bis zu yield und dann laufen zu lassen?

Ich kenne die Möglichkeit, nur return tuple(func(self, *args, **kwargs)) aufrufen, aber ich möchte vermeiden, alle Datensätze auf einmal zu laden.

Antwort

6

Zuerst denke ich, dass die Lösung, die Sie gerade verwenden, in Ordnung ist. Wenn Sie einen Generator dekorieren, muss sich der Dekorator zumindest wie ein Iterator über diesen Generator verhalten. Das zu tun, indem man den Dekorator zu einem Generator macht, ist vollkommen in Ordnung. Wie x3al darauf hingewiesen hat, ist die Verwendung von yield from func(...) anstelle von for row in func(...): yield row eine mögliche Optimierung.

Wenn Sie auch vermeiden möchten, dass der Decorator tatsächlich zu einem Generator wird, können Sie dies tun, indem Sie next verwenden, der bis zum ersten yield ausgeführt wird, und den ersten ausgegebenen Wert zurückgeben. Sie müssen den Decorator dazu bringen, diesen ersten Wert zu erfassen und zurückzugeben, zusätzlich zu den restlichen Werten, die vom Generator geliefert werden. Sie tun können, dass mit 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 

Sie auch die Dekorateur Arbeit mit beiden Generatoren und nicht-Generator-Funktionen machen könnte, inspect, um festzustellen, ob Sie einen Generator sind die Dekoration:

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 

I würde die yield/yield from-basierte Lösung bevorzugen, es sei denn, Sie haben eine Anforderung, neben Generatoren regelmäßige Funktionen zu dekorieren.

+0

Ich mag diese Antwort (+ 1-ed), aber wie wäre es mit dem Fall, wenn 'next (gen) 'erhöht' StopIteration'? – Vyktor

+0

@Vyktor Ich habe meine Antwort bearbeitet, um diesen Fall zu behandeln. Sie können nur diese Ausnahme abfangen und das Generator-Objekt zurückgeben, das keine Operation ausführt, wenn ein Versuch unternommen wird, darüber zu iterieren (oder "StopIteration" erneut zu erhöhen, wenn "nächste" aufgerufen wird). – dano

+0

Ehrlich gesagt, das ist der Grund, warum ich vorgeschlagen habe, vor der Schleife in 'get_data' noch einen' yield' hinzuzufügen. Die Lösung des Dano ist jedoch viel klarer. – x3al

3

Gibt es eine Möglichkeit, Python den gesamten Code bis zur ersten Ausbeute laufen zu lassen und dann zu warten?

Ja und es heißt next(your_generator). Rufen Sie next() einmal und der Code wird genau nach dem ersten yield warten. Sie können einen weiteren yield direkt vor der Schleife platzieren, wenn Sie den ersten Wert nicht verlieren möchten.

Wenn Sie Python 3.3+ verwenden, können Sie auch

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

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

+2

Sie sollten 'next (your_generator)' verwenden, 'obj.next()' wurde in Python 3.x entfernt – dano