__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