guidot
guidot

Reputation: 5333

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?

Upvotes: 3

Views: 41

Answers (2)

Raymond Hettinger
Raymond Hettinger

Reputation: 226486

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

Upvotes: 5

Netwave
Netwave

Reputation: 42746

Just implementing the __getItem__ method python will try to acces all indexes from 0 to infinite, so if you dont specify when to stop it will keep calling the method.

Just check if the index value is higher than the size for example:

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

    def __len__(self):
        return self.size

    def __getitem__(self, item):
      if item < self.size:
        return item*10
      raise IndexError

Here you have a live example

Upvotes: 2

Related Questions