for loop is syntactic sugar. The following code:
for <var> in <obj>: <code>
is equivalent to:
i = iter(<obj>) while True: try: <var> = next(i) except StopIteration: break <code>
This even correctly handles
In the above code what the built-ins
next(bar) do is (essentially) call the
bar.__next__() dunder methods and return their results.
An iterable in Python is, unsurprisingly, an object on which one can iterate. By that it is meant that it’s an object that can be used in a
for loop by being put in place of the
<obj> in the code above. Thus all that an iterable is required to implement is a
__iter__() method. This method must return something called an iterator. Looking again at the code above, all an iterator must do is expose a
__next__() method which either returns the next element to iterate on or raises
StopIteration if no elements are left.
There’s one small catch: one could at times have an iterator without the iterable that generated it and, if iterators don’t implement the
__iter__() method they can’t be passed to a
for loop and are thus quite useless; it is therefore required for an iterator to also have an
__iter__() method that simply returns the object itself.
That’s all there is to it.
Let’s try to reimplement the built-in
range object. First, observe that calling
range doesn’t give an iterator but an iterable, from which we can generate as many iterators as we want:
>>> r = range(3) >>> list(r) + list(r) [0, 1, 2, 0, 1, 2]
Thus we must implement an iterable and an iterator. Let’s start with the former:
class range_iterable: def __init__(self, stop): self.stop = stop def __iter__(self): return range_iterator(self.stop)
For brevity we don’t support
step arguments. Now to the second part, the iterator:
class range_iterator: def __init__(self, stop): self.cur = 0 self.stop = stop def __iter__(self): return self def __next__(self): if self.cur == self.stop: raise StopIteration() res = self.cur self.cur += 1 return res
Does this work?
>>> r = range_iterable(3) >>> list(r) + list(r) [0, 1, 2, 0, 1, 2] >>> r = range_iterator(3) >>> list(r) + list(r) [0, 1, 2]
Yes, it does!