Search code examples
pythonpython-2.7iteratorpython-2.xpython-internals

“iter() returned non-iterator” for dynamically bound `next` method


Why does this next method I dynamically bound to an instance of a class fail and return a non-iterator object?

from collections import Iterator
from collections import Iterable
from types import MethodType

def next(cls):
    if cls.start < cls.stop:
        cls.start += 1
        return cls.start
    else:
        raise StopIteration


class Foo(object):
    start, stop = 0, 5

    def __iter__(self):
        return self

if __name__ == "__main__":
    foo = Foo()
    setattr(foo, 'next', MethodType(next, foo, Foo))
    print hasattr(foo, "next")
    if isinstance(foo, Iterable):
        print "iterable"
    if isinstance(foo, Iterator):
        print "iterator"

    for i in foo:
        print i

Output:

iterable
True
TypeError: iter() returned non-iterator of type 'Foo'

It worked properly, when I did setattr(Foo, 'next', classmethod(next)).


Solution

  • for i in foo:
        print i
    

    This is the code that failed, so let’s take a look at what happens internally there, diving into the source for some Python internals!

    When for i in foo is compiled, the generated bytecode will contain the GET_ITER opcode which is responsible of converting foo into an iterable. GET_ITER causes a PyObject_GetIter call on the object which is the actual implementation that provides the iterable. So let’s take a look at what it does:

    PyObject * PyObject_GetIter(PyObject *o)
    {
        PyTypeObject *t = o->ob_type;
        getiterfunc f = NULL;
        if (PyType_HasFeature(t, Py_TPFLAGS_HAVE_ITER))
            f = t->tp_iter;
        if (f == NULL) {
            if (PySequence_Check(o))
                return PySeqIter_New(o);
            return type_error("'%.200s' object is not iterable", o);
        }
        else {
            PyObject *res = (*f)(o);
            if (res != NULL && !PyIter_Check(res)) {
                PyErr_Format(PyExc_TypeError,
                             "iter() returned non-iterator of type '%.100s'",
                             res->ob_type->tp_name);
                Py_DECREF(res);
                res = NULL;
            }
            return res;
        }
    }
    

    As you can see (if you understand some basic C at least), the underlying type is looked up from the object first (o->ob_type), and then its iter function is read (t->tp_iter).

    Since you have implemented an __iter__ function on the type, this function does exist, so we get to the else case in the code above, which runs the iter function on the object o. The result is non-null, but we still get the “returned non-iterator” message, so the PyIter_Check(res) appears to have failed. So let’s take a look at what that does:

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

    So this one essentially checks if the type (ob_type) of the passed object has a non-null next method (tp_iternext) which does not happen to be the not-implemented next function.

    Check closely where that check happens though: On the result’s type, not the result itself. The foo object does have a next function, but its type Foo does not have one.

    setattr(foo, 'next', MethodType(next, foo, Foo))
    

    … or more explicitly …

    foo.next = next.__get__(foo, Foo)
    

    … does only set the bound next method on the instance but not on the type itself. So the above C code will fail consuming it as an iterable.

    If you were to set your next function on the type instead, it would work fine:

    foo = Foo()
    Foo.next = next
    
    for i in foo:
        print i
    

    This is the reason why your attempt with classmethod worked: You set the function on the type instead of its concrete instance.