2013-03-12 14 views
18

In python, le istruzioni with devono essere utilizzate all'interno di un generatore? Per essere chiari, non sto chiedendo di usare un decoratore per creare un gestore di contesto da una funzione generatore. Sto chiedendo se c'è un problema inerente usando un'istruzione with come un gestore di contesto all'interno di un generatore in quanto prenderà le eccezioni StopIteration e GeneratorExit in almeno alcuni casi. Seguono due esempi.Come utilizzare un gestore di contesto python all'interno di un generatore

Un buon esempio del problema viene sollevato dall'esempio di Beazley (pagina 106). L'ho modificato per utilizzare un'istruzione with in modo che i file vengano chiusi esplicitamente dopo il rendimento nel metodo di apertura. Ho anche aggiunto due modi in cui un'eccezione può essere lanciata mentre si ripetono i risultati.

import os 
import fnmatch 

def find_files(topdir, pattern): 
    for path, dirname, filelist in os.walk(topdir): 
     for name in filelist: 
      if fnmatch.fnmatch(name, pattern): 
       yield os.path.join(path,name) 
def opener(filenames): 
    f = None 
    for name in filenames: 
     print "F before open: '%s'" % f 
     #f = open(name,'r') 
     with open(name,'r') as f: 
      print "Fname: %s, F#: %d" % (name, f.fileno()) 
      yield f 
      print "F after yield: '%s'" % f 
def cat(filelist): 
    for i,f in enumerate(filelist): 
     if i ==20: 
      # Cause and exception 
      f.write('foobar') 
     for line in f: 
      yield line 
def grep(pattern,lines): 
    for line in lines: 
     if pattern in line: 
      yield line 

pylogs = find_files("/var/log","*.log*") 
files = opener(pylogs) 
lines = cat(files) 
pylines = grep("python", lines) 
i = 0 
for line in pylines: 
    i +=1 
    if i == 10: 
     raise RuntimeError("You're hosed!") 

print 'Counted %d lines\n' % i 

In questo esempio, il gestore di contesto chiude correttamente i file nella funzione di apertura. Quando viene sollevata un'eccezione, rilevo la traccia dall'eccezione, ma il generatore si blocca silenziosamente. Se l'istruzione with ottiene l'eccezione, perché il generatore non continua?

Quando definisco i miei gestori di contesto da utilizzare all'interno di un generatore. Ottengo errori di runtime dicendo che ho ignorato un GeneratorExit. Per esempio:

class CManager(object): 
    def __enter__(self): 
      print " __enter__" 
      return self 
    def __exit__(self, exctype, value, tb): 
     print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value) 
     return True 

def foo(n): 
    for i in xrange(n): 
     with CManager() as cman: 
      cman.val = i 
      yield cman 
# Case1 
for item in foo(10): 
    print 'Pass - val: %d' % item.val 
# Case2 
for item in foo(10): 
    print 'Fail - val: %d' % item.val 
    item.not_an_attribute 

Questo piccolo demo funziona bene in case1 senza eccezioni sollevate, ma non riesce a case2 in cui viene generato un errore attributo. Qui vedo un RuntimeException sollevato perché l'istruzione with ha catturato e ignorato un'eccezione GeneratorExit.

Qualcuno può aiutare a chiarire le regole per questo astuto caso d'uso? Sospetto che sia qualcosa che sto facendo o non sto facendo nel mio metodo __exit__. Ho provato ad aggiungere codice per controrilanciare GeneratorExit, ma quello non ha aiutato.

risposta

10

dal Data model entry for object.__exit__

Se un'eccezione viene alimentato, e il metodo desidera sopprimere l'eccezione (ossia impedirne propagato), deve restituire un valore vero. Altrimenti, l'eccezione verrà elaborata normalmente all'uscita da questo metodo.

Nella funzione __exit__, stai tornando True che sopprimere tutti eccezioni. Se lo modifichi per restituire False, le eccezioni continueranno a essere sollevate normalmente (con la sola differenza che garantisci che la tua funzione __exit__ viene chiamata e puoi fare in modo di ripulire dopo te stesso)

Ad esempio, modificando il codice a:

def __exit__(self, exctype, value, tb): 
    print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value) 
    if exctype is GeneratorExit: 
     return False 
    return True 

ti permette di fare la cosa giusta e non sopprimere il GeneratorExit. Ora si solo vedere l'errore di attributo. Forse la regola generale dovrebbe essere la stessa di qualsiasi altra gestione delle eccezioni - intercetta solo eccezioni se sai come gestirle. Avere un __exit__ ritorno True è alla pari (forse leggermente peggio!) Che avere una nuda ad eccezione di:

try: 
    something() 
except: #Uh-Oh 
    pass 

Si noti che quando il AttributeError è maggiore (e non catturato), credo che fa sì che il conteggio dei riferimenti sul vostro oggetto generatore a goccia a 0 che poi innesca un GeneratorExit eccezione all'interno del generatore in modo che possa ripulirsi. Usando il mio __exit__, giocare con i seguenti due casi e, auspicabilmente, vedrete cosa intendo:

try: 
    for item in foo(10): 
     print 'Fail - val: %d' % item.val 
     item.not_an_attribute 
except AttributeError: 
    pass 

print "Here" #No reference to the generator left. 
       #Should see __exit__ before "Here" 

e

g = foo(10) 
try: 
    for item in g: 
     print 'Fail - val: %d' % item.val 
     item.not_an_attribute 
except AttributeError: 
    pass 

print "Here" 
b = g #keep a reference to prevent the reference counter from cleaning this up. 
     #Now we see __exit__ *after* "Here" 
+1

Questa è una risposta impressionante! – lukecampbell

+0

@mgilson Grazie per la tua grande risposta. Sembra che la funzione del generatore non sia ciò che cattura l'errore di attributo. Questo è il comportamento che volevo, ma sembra che non sia possibile. Voglio usare una sola istruzione per centralizzare la gestione delle eccezioni in una sequenza di generatori. – David

+0

@David - i context manager sono qualcosa che ho lavorato con una quantità decente negli ultimi tempi, quindi sono freschi nella mia mente :). E 'stata una buona domanda. Auguro a tutti i nuovi utenti di porre queste belle prime domande :). – mgilson

1
class CManager(object): 
    def __enter__(self): 
      print " __enter__" 
      return self 
    def __exit__(self, exctype, value, tb): 
     print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value) 
     if exctype is None: 
      return 

     # only re-raise if it's *not* the exception that was 
     # passed to throw(), because __exit__() must not raise 
     # an exception unless __exit__() itself failed. But throw() 
     # has to raise the exception to signal propagation, so this 
     # fixes the impedance mismatch between the throw() protocol 
     # and the __exit__() protocol. 
     # 
     if sys.exc_info()[1] is not (value or exctype()): 
      raise 
+0

I documenti dichiarano che '__exit__' non dovrebbe sollevare di nuovo l'eccezione passata in ... – mgilson

+0

@mgilson, Odd ... Ho pizzicato l'idea da contextlib.py –

+0

Per essere onesti, non lo so perché tu * non dovresti * ... Anche se non so perché vorrai o. Se si restituisce un valore 'False-y', l'eccezione continua a propagarsi comunque. – mgilson

Problemi correlati