__next__ and StopIteration

Steven D'Aprano steve+comp.lang.python at pearwood.info
Mon Feb 9 18:30:14 EST 2015


Charles Hixson wrote:

> I'm trying to write a correct iteration over a doubly indexed container,
> and what I've got so far is:    def __next__ (self):
>          for row    in    range(self._rows):
>              for col in range(self._cols):
>                  if self._grid[row][col]:
>                      yield    self._grid[row][col]
>                  #end    if
>              #end    for col
>          #end    for row
>          raise    StopIteration

That's wrong, you don't use yield in __next__ as that will turn it into a
generator function. Here is a simplified example demonstrating the problem:


py> class X(object):
...     def __next__(self):
...             yield 1
...             yield 2
...
py> x = X()
py> next(x)
<generator object __next__ at 0xb7b0e504>
py> next(x)
<generator object __next__ at 0xb7b0e52c>
py> next(x)
<generator object __next__ at 0xb7b0e504>
py> next(x)
<generator object __next__ at 0xb7b0e52c>


The way you write iterators is like this:


Method 1 (the hard way):

- Give your class an __iter__ method which simply returns self:

    def __iter__(self):
        return self

- Give your class a __next__ method (`next` in Python 2) which *returns* a
value. You will need to track which value to return yourself. It must raise
StopIteration when there are no more values to return. Don't use yield.

    def __next__(self):
        value = self.value
        if value is None:
            raise StopIteration
        self.value = self.calculate_the_next_value()
        return value

Your class is itself an iterator.

Method 2 (the easy way):

- Don't write a __next__ method at all.

- Give your class an __iter__ method which returns an iterator. E.g.:

    def __iter__(self):
        return iter(self.values)

In this case, your class is not itself an iterator, but it is iterable:
calling iter(myinstance) will return an iterator, which is enough.

If you don't have a convenient collection of values to return, you can
conveniently use a generator, yielding values you want and just falling off
the end (or returning) when you are done. E.g.:

    def __iter__(self):
        while self.value is not None:
            yield self.value
            self.calculate_the_next_value()

In your case, it looks to me that what you need is something like:


    def __iter__(self):
        for row in range(self._rows):
            for col in range(self._cols):
                if self._grid[row][col]:
                    yield self._grid[row][col]

which is probably better written as:

    # untested -- I may have the row/col order backwards
    def __iter__(self):
        for column in self._grid:
            for item in column:
                if value:
                    yield value


As a general rule, Python is not Fortran. If you find yourself wanting to
write code that iterates over an index, then indexes into a list or array,
99.9% of the time you are better off just iterating over the list or array
directly:

# not this!
for i in range(len(mylist)):
    value = mylist[i]
    print(value)

# instead use this
for value in mylist:
    print(value)


If you need both the index and the list item, use enumerate:

for i, value in enumerate(mylist):
    print(i, value)



-- 
Steven




More information about the Python-list mailing list