2009-07-20 14 views
12

Como parte de algún middleware de WSGI, quiero escribir una clase de python que envuelva un iterador para implementar un método de cierre en el iterador.iteradores de Python: ¿cómo asignar dinámicamente self.next dentro de una nueva clase de estilo?

Esto funciona bien cuando lo intento con una clase antigua, pero arroja un TypeError cuando lo intento con una clase de estilo nuevo. ¿Qué debo hacer para que esto funcione con una clase de nuevo estilo?

Ejemplo:

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 

da el siguiente resultado:

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

Respuesta

9

Lo que estás tratando de hacer tiene sentido, pero aquí está pasando algo mal dentro de Python.

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() es un iterador que modifica su siguiente método en el momento - perfectamente legal en cualquier otro lugar en Python. El iterador que creamos tiene el método que esperamos: it.next es next2. Cuando usamos el iterador directamente, al llamar a next(), obtenemos 2. Sin embargo, cuando lo usamos en un ciclo for, obtenemos el original siguiente, que hemos sobrescrito claramente.

No estoy familiarizado con las partes internas de Python, pero parece que el método "siguiente" de un objeto se almacena en caché en tp_iternext (http://docs.python.org/c-api/typeobj.html#tp_iternext) y luego no se actualiza cuando se cambia la clase.

Esto es definitivamente un error de Python. Tal vez esto se describe en los PEP del generador, pero no está en la documentación básica de Python, y es completamente inconsistente con el comportamiento normal de Python.

Se puede evitar esto manteniendo la función siguiente original y envolviéndolo de forma explícita:

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 

... pero eso es, obviamente, menos eficiente, y usted debe no tener que hacer eso.

+0

¿Ha provocado un error en http://bugs.python.org? –

3

sólo devuelve el iterador. Eso es lo que __iter__ es para. No tiene sentido intentar aplicar el parche de mono al objeto en el iterador y devolverlo cuando ya tienes un iterador.

EDITAR: Ahora con dos métodos. Una vez, el parche parcheando el iterador envuelto, segundo, el envoltorio del iterador.

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

Ambos métodos funcionan tanto con las clases nuevas como antiguas.

+0

Gracias por su respuesta! Tal vez debería haber dado más contexto: la razón por la que tengo una clase contenedora es para que pueda agregar un método cercano al objeto iterador devuelto, y simplemente devolver el iterador original no me da la oportunidad de hacerlo. – ollyc

+0

Puedo pensar en varios casos en los que podría querer hacer esto. Todos son artificiales (es decir, nunca he tenido que hacer esto), así que no me molestaré en enumerarlos, pero esta es una cosa perfectamente válida que quiero hacer. –

+0

Además, esto no es parche de monos. El parche de mono está modificando los métodos de una clase fuera de la clase en sí, no una clase que modifica sus propios métodos. –

4

Parece incorporada iter no comprueba next exigible en una instancia, pero en una clase y IteratorWrapper2 no tiene ningún next. A continuación se muestra la versión más simple de su 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 

lo que la solución sería volver otheriter en __iter__

class IteratorWrapper2(object): 

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

    def __iter__(self): 
     return self.otheriter 

o danos tu next, envolviendo iterador interno

class IteratorWrapper2(object): 

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

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

    def __iter__(self): 
     return self 

Aunque me No entiendo por qué no iter solo usa el self.next de instancia.

6

Hay un montón de lugares donde CPython tomar atajos sorprendentes sobre la base de la clase propiedades en lugar de ejemplo propiedades. Este es uno de esos lugares.

Aquí está un ejemplo sencillo que muestra el problema:

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

Y aquí es lo que sucede:

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

Ahora, cavando en el código fuente CPython (de 2.7.2), aquí está el aplicación de la orden interna next():

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; 
    } 
    … 
} 

Y aquí está la implementatio n de 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 primera línea, PyType_HasFeature(…), es decir, después de expandirse en todas las constantes y macros y esas cosas, lo que equivale a DynamicNext.__class__.__flags__ & 1L<<17 != 0:

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

Así que, obviamente, no es cheque fallando ... ¿Qué debe significar que el próximo cheque - (obj)->ob_type->tp_iternext != NULL - está fallando.

En Python, esta línea es más o menos (más o menos!) Equivalente a hasattr(type(instance), "next"):

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

Lo que obviamente falla porque el tipo DynamicNext no tiene un método next - únicos casos de ese tipo hacer.

Ahora, mi CPython foo es débil, así que voy a tener que empezar a hacer algunas conjeturas aquí ... Pero creo que son precisas.

Cuando se crea un tipo CPython (es decir, cuando el intérprete se evalúa primero el bloque de class y la clase metaclase __new__ método se llama), los valores de PyTypeObject estructura del tipo se inician al ... Así que si, cuando el DynamicNext se crea el tipo, no existe el método next, el campo tp_iternext se establecerá en NULL, lo que hará que PyIter_Check devuelva falso.

Ahora bien, como señala el Glenn, esto es casi seguro que un error en CPython ... Especialmente teniendo en cuenta que la corrección sería sólo el rendimiento impacto cuando ya sea el objeto que está siendo probado no es iterable o dinámicamente asigna un método next (muy aproximadamente):

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

Editar: después de un poco de excavación, la solución no sería tan sencilla, ya que al menos algunas partes del código suponen que, si PyIter_Check(it) vuelve true, entonces existirá *it->ob_type->tp_iternext ... Lo cual no es necesario con dificultad el caso (es decir, porque la función next existe en la instancia, no el tipo).

SO! Es por eso que suceden cosas sorprendentes cuando intenta iterar sobre una instancia de estilo nuevo con un método next dinámicamente asignado.

+0

¿Te importaría plantearlo como un error en CPython? –

Cuestiones relacionadas