Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pseudo-sequence object: Exception needed to terminate?

I lived with the assumption, that defining the _ _len_ _ and _ _getitem_ _ methods are sufficient for a class, so its instances can be iterated over via a for element in instance:, until it got violated an example. My original code is quite different, but this shows the problem nicely entering an endless loop:

class Limited(object):
    def __init__(self, size=5):
        self.size = size

    def __len__(self):
        return self.size

    def __getitem__(self, item):
        return item*10

if __name__ == "__main__":
    test = Limited(4)
    assert len(test) == 4

    for q in test:
        print q

I could not find a specific reference to the requirements for terminating an iteration loop, but it seems that an exception like an IndexError or StopIteration is necessary for termination, if one does not want to comply to full Iterator protocol.

Is that correct and where to find it documented?

like image 492
guidot Avatar asked Jun 16 '26 21:06

guidot


1 Answers

Answer

Yes, an IndexError is required to terminate.

Documentation

See the documentation for __getitem__() which has the note:

Note for loops expect that an IndexError will be raised for illegal indexes to allow proper detection of the end of the sequence.

Underlying source code

The logic for creating an iterator is in Objects/iterobject.c:

static PyObject *
iter_iternext(PyObject *iterator)
{
    seqiterobject *it;
    PyObject *seq;
    PyObject *result;

    assert(PySeqIter_Check(iterator));
    it = (seqiterobject *)iterator;
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;
    if (it->it_index == PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
                        "iter index too large");
        return NULL;
    }

    result = PySequence_GetItem(seq, it->it_index);
    if (result != NULL) {
        it->it_index++;
        return result;
    }
    if (PyErr_ExceptionMatches(PyExc_IndexError) ||
        PyErr_ExceptionMatches(PyExc_StopIteration))
    {
        PyErr_Clear();
        Py_DECREF(seq);
        it->it_seq = NULL;
    }
    return NULL;
}

Worked out example

To fix the OP's code, only two lines need to be added to the beginning of the __getitem__() method:

class Limited(object):
    def __init__(self, size=5):
        self.size = size

    def __len__(self):
        return self.size

    def __getitem__(self, item):
        if item >= len(self):
            raise IndexError
        return item*10

if __name__ == "__main__":
    test = Limited(4)
    assert len(test) == 4

    for q in test:
        print(q)

This outputs a finite sequence:

0
10
20
30
like image 147
Raymond Hettinger Avatar answered Jun 19 '26 12:06

Raymond Hettinger