2009-07-20 5 views
12

Come parte di alcuni middleware WSGI, desidero scrivere una classe python che includa un iteratore per implementare un metodo close sull'iteratore.iteratori di Python: come assegnare dinamicamente self.next all'interno di una nuova classe di stile?

Questo funziona bene quando provo con una classe vecchio stile, ma genera un errore TypeE quando lo provo con una classe di nuovo stile. Cosa devo fare per farlo funzionare con una classe di nuovo stile?

Esempio:

class IteratorWrapper1: 

    def __init__(self, otheriter): 
     self._iterator = otheriter 
     self.next = otheriter.next 

    def __iter__(self): 
     return self 

    def close(self): 
     if getattr(self._iterator, 'close', None) is not None: 
      self._iterator.close() 
     # other arbitrary resource cleanup code here 

class IteratorWrapper2(object): 

    def __init__(self, otheriter): 
     self._iterator = otheriter 
     self.next = otheriter.next 

    def __iter__(self): 
     return self 

    def close(self): 
     if getattr(self._iterator, 'close', None) is not None: 
      self._iterator.close() 
     # other arbitrary resource cleanup code here 

if __name__ == "__main__": 
    for i in IteratorWrapper1(iter([1, 2, 3])): 
     print i 

    for j in IteratorWrapper2(iter([1, 2, 3])): 
     print j 

ha pronunciato la seguente uscita:

1 
2 
3 
Traceback (most recent call last): 
    ... 
TypeError: iter() returned non-iterator of type 'IteratorWrapper2' 

risposta

9

Quello che stai cercando di fare ha senso, ma c'è qualcosa di malvagio in Python qui.

class foo(object): 
    c = 0 
    def __init__(self): 
     self.next = self.next2 

    def __iter__(self): 
     return self 

    def next(self): 
     if self.c == 5: raise StopIteration 
     self.c += 1 
     return 1 

    def next2(self): 
     if self.c == 5: raise StopIteration 
     self.c += 1 
     return 2 

it = iter(foo()) 
# Outputs: <bound method foo.next2 of <__main__.foo object at 0xb7d5030c>> 
print it.next 
# 2 
print it.next() 
# 1?! 
for x in it: 
    print x 

foo() è un iteratore che modifica il suo metodo successivo al volo - perfettamente legale in qualsiasi altro luogo in Python. L'iteratore che creiamo, ha il metodo che ci aspettiamo: it.next è next2. Quando usiamo direttamente l'iteratore, chiamando next(), otteniamo 2. Tuttavia, quando lo usiamo in un ciclo for, otteniamo l'originale successivo, che abbiamo chiaramente sovrascritto.

io non sono a conoscenza di Python interni, ma sembra che il metodo "prossimo" di un oggetto viene memorizzato nella cache in tp_iternext (http://docs.python.org/c-api/typeobj.html#tp_iternext), e allora non è aggiornato quando la classe è cambiato.

Questo è sicuramente un bug Python. Forse questo è descritto nel generatore PEP, ma non è nella documentazione di base di Python, ed è completamente incoerente con il normale comportamento di Python.

Si potrebbe ovviare a questo mantenendo la funzione originale successivo e avvolgendolo in modo esplicito:

class IteratorWrapper2(object): 
    def __init__(self, otheriter): 
     self.wrapped_iter_next = otheriter.next 
    def __iter__(self): 
     return self 
    def next(self): 
     return self.wrapped_iter_next() 

for j in IteratorWrapper2(iter([1, 2, 3])): 
    print j 

... ma questo è ovviamente meno efficiente, e si dovrebbe non avere a che fare.

+0

Hai sollevato un bug su http://bugs.python.org? –

3

Basta tornare l'iteratore. Questo è ciò che è __iter__. Non ha senso provare a eseguire il patch delle scimmie con l'oggetto in iteratore e restituirlo quando hai già un iteratore.

MODIFICA: ora con due metodi. Una volta, la scimmia rattoppa l'iteratore avvolto, secondo, il gattino avvolge l'iteratore.

class IteratorWrapperMonkey(object): 

    def __init__(self, otheriter): 
     self.otheriter = otheriter 
     self.otheriter.close = self.close 

    def close(self): 
     print "Closed!" 

    def __iter__(self): 
     return self.otheriter 

class IteratorWrapperKitten(object): 

    def __init__(self, otheriter): 
     self.otheriter = otheriter 

    def __iter__(self): 
     return self 

    def next(self): 
     return self.otheriter.next() 

    def close(self): 
     print "Closed!" 

class PatchableIterator(object): 

    def __init__(self, inp): 
     self.iter = iter(inp) 

    def next(self): 
     return self.iter.next() 

    def __iter__(self): 
     return self 

if __name__ == "__main__": 
    monkey = IteratorWrapperMonkey(PatchableIterator([1, 2, 3])) 
    for i in monkey: 
     print i 
    monkey.close() 

    kitten = IteratorWrapperKitten(iter([1, 2, 3])) 
    for i in kitten: 
     print i 
    kitten.close() 

Entrambi i metodi funzionano con classi nuove e di vecchio stile.

+0

Grazie per la risposta! Forse avrei dovuto dare più contesto: il motivo per cui ho una classe wrapper è che posso aggiungere un metodo vicino all'oggetto iteratore restituito, e restituire l'iteratore originale non mi dà la possibilità di farlo. – ollyc

+0

Posso pensare a diversi casi in cui potrei voler fare questo. Sono tutti inventati (vale a dire, non ho mai dovuto farlo), quindi non mi preoccuperò di elencarli, ma questa è una cosa assolutamente valida da voler fare. –

+0

Inoltre, questo non è patch per scimmia. Il patch delle scimmie sta modificando i metodi di una classe al di fuori della classe stessa, non una classe che modifica i propri metodi. –

4

Sembra incorporato iter non verificare la presenza di next richiamabile in un caso, ma in una classe e IteratorWrapper2 non ha alcun next. Qui di seguito è la versione più semplice del problema

class IteratorWrapper2(object): 

    def __init__(self, otheriter): 
     self.next = otheriter.next 

    def __iter__(self): 
     return self 

it=iter([1, 2, 3]) 
myit = IteratorWrapper2(it) 

IteratorWrapper2.next # fails that is why iter(myit) fails 
iter(myit) # fails 

quindi la soluzione sarebbe quella di tornare otheriter in __iter__

class IteratorWrapper2(object): 

    def __init__(self, otheriter): 
     self.otheriter = otheriter 

    def __iter__(self): 
     return self.otheriter 

o lascia la tua next, avvolgendo iteratore interno

class IteratorWrapper2(object): 

    def __init__(self, otheriter): 
     self.otheriter = otheriter 

    def next(self): 
     return self.otheriter.next() 

    def __iter__(self): 
     return self 

Anche se lo faccio non capisco perchénon usi semplicemente il self.next di istanza.

6

Ci sono un sacco di posti in cui CPython prendere scorciatoie sorprendenti sulla base di classe proprietà invece di esempio proprietà. Questo è uno di quei posti.

Ecco un semplice esempio che illustra il problema:

def DynamicNext(object): 
    def __init__(self): 
     self.next = lambda: 42 

Ed ecco cosa succede:

 
>>> instance = DynamicNext() 
>>> next(instance) 
… 
TypeError: DynamicNext object is not an iterator 
>>> 

Ora, scavando nel codice sorgente CPython (da 2.7.2), ecco la attuazione del next() incorporato:

static PyObject * 
builtin_next(PyObject *self, PyObject *args) 
{ 
    … 
    if (!PyIter_Check(it)) { 
     PyErr_Format(PyExc_TypeError, 
      "%.200s object is not an iterator", 
      it->ob_type->tp_name); 
     return NULL; 
    } 
    … 
} 

Ed ecco l'implementatio n di PyIter_Check:

#define PyIter_Check(obj) \ 
    (PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \ 
    (obj)->ob_type->tp_iternext != NULL && \ 
    (obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented) 

La prima linea, PyType_HasFeature(…), è, dopo l'espansione tutte le costanti e macro e roba, equivalenti a DynamicNext.__class__.__flags__ & 1L<<17 != 0:

 
>>> instance.__class__.__flags__ & 1L<<17 != 0 
True 

Così che il check, ovviamente, non è fallendo ... Quale deve significare che il prossimo controllo - (obj)->ob_type->tp_iternext != NULL - è in errore.

In Python, questa linea è grosso modo (più o meno!) Equivalente a hasattr(type(instance), "next"):

 
>>> type(instance) 
__main__.DynamicNext 
>>> hasattr(type(instance), "next") 
False 

che non riesce, ovviamente, perché il tipo di DynamicNext non ha un metodo next - solo le istanze di quel tipo fare.

Ora, il mio CPython foo è debole, quindi dovrò iniziare a fare alcune ipotesi plausibili qui ... Ma credo che siano accurate.

Quando si crea un tipo CPython (cioè, quando l'interprete prima valuta il blocco class e la classe metaclasse __new__ metodo viene chiamato), i valori sul PyTypeObject struct del tipo vengono inizializzati ... Quindi, se, quando il DynamicNext il tipo è stato creato, non esiste il metodo next, il campo tp_iternext, verrà impostato su NULL, provocando PyIter_Check per restituire false.

Ora, come sottolinea il Glenn, questo è quasi certamente un bug in CPython ... Soprattutto in considerazione che la correzione sarebbe solo impatto sulle prestazioni quando sia l'oggetto in fase di sperimentazione non è iterabile o assegna un next metodo (molto dinamicamente circa):

#define PyIter_Check(obj) \ 
    (((PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \ 
     (obj)->ob_type->tp_iternext != NULL && \ 
     (obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented)) || \ 
     (PyObject_HasAttrString((obj), "next") && \ 
     PyCallable_Check(PyObject_GetAttrString((obj), "next")))) 

Edit: dopo un po 'di scavo, la correzione non sarebbe questo semplice, perché almeno alcune porzioni del codice per scontato che, se PyIter_Check(it) rendimenti true, quindi *it->ob_type->tp_iternext esisteranno ... Che non è neces sarily caso (cioè, perché la funzione next esiste sull'istanza, non il tipo).

SO! Ecco perché le cose sorprendenti accadono quando si tenta di scorrere su un'istanza di nuovo stile con un metodo next dinamicamente assegnato.

+0

Ti piacerebbe aumentarlo come un bug in CPython? –

Problemi correlati