Search code examples
pythonpython-2.7monkeypatchingforbiddenfruit

Make int iterable with forbiddenfruit


I know, this is wrong, but is it possible? I thought an object is considered an iterable when its .__iter__ method returned an iterator? So why doesn't this work?

>>> from forbiddenfruit import curse
>>> def __iter__(self):
...     for i in range(self):
...         yield i
>>> curse(int, "__iter__", __iter__)
>>> for x in 5:
...     print x
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable

int does seem to have an __iter__ method now:

>>> int(5).__iter__
<bound method int.__iter__ of 5>

Solution

  • The disassembly of a for loop is:

    import dis
    
    dis.dis("for _ in _: pass")
    #>>>   1           0 SETUP_LOOP              14 (to 17)
    #>>>               3 LOAD_NAME                0 (_)
    #>>>               6 GET_ITER
    #>>>         >>    7 FOR_ITER                 6 (to 16)
    #>>>              10 STORE_NAME               0 (_)
    #>>>              13 JUMP_ABSOLUTE            7
    #>>>         >>   16 POP_BLOCK
    #>>>         >>   17 LOAD_CONST               0 (None)
    #>>>              20 RETURN_VALUE
    

    So we want the GET_ITER opcode.

    TARGET(GET_ITER) {
        /* before: [obj]; after [getiter(obj)] */
        PyObject *iterable = TOP();
        PyObject *iter = PyObject_GetIter(iterable);
        Py_DECREF(iterable);
        SET_TOP(iter);
        if (iter == NULL)
            goto error;
        PREDICT(FOR_ITER);
        DISPATCH();
    }
    

    Which uses PyObject_GetIter:

    PyObject *
    PyObject_GetIter(PyObject *o)
    {
        PyTypeObject *t = o->ob_type;
        getiterfunc f = NULL;
        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;
        }
    }
    

    This first checks t->tp_iter for nullity.

    Now, here's the thing that makes everything click:

    class X:
        pass
    
    X.__iter__ = lambda x: iter(range(10))
    
    list(X())
    #>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    from forbiddenfruit import curse
    
    class X:
        pass
    
    curse(X, "__iter__", lambda x: iter(range(10)))
    
    list(X())
    #>>> Traceback (most recent call last):
    #>>>   File "", line 16, in <module>
    #>>> TypeError: 'X' object is not iterable
    

    When you set an attribute on a class normally, it calls PyType_Type->setattro:

    static int
    type_setattro(PyTypeObject *type, PyObject *name, PyObject *value)
    {
        if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
            PyErr_Format(
                PyExc_TypeError,
                "can't set attributes of built-in/extension type '%s'",
                type->tp_name);
            return -1;
        }
        if (PyObject_GenericSetAttr((PyObject *)type, name, value) < 0)
            return -1;
        return update_slot(type, name);
    }
    

    See update_slot? That goes and updates the slot, so the next call to GET_ITER will hit tp->tp_iter on X. However, forbiddenfruit bypasses this process and just injects a dictionary into the class. This means that PyLong_Type keeps its default:

    PyTypeObject PyLong_Type = {
        ...
        0,                                          /* tp_iter */
        ...
    };
    

    So

    if (f == NULL)
    

    gets triggered,

    if (PySequence_Check(o))
    

    fails (since it's not a sequence) and then it's just

    return type_error("'%.200s' object is not iterable", o);