When you write a
for loop, Python does more work than it looks like. It calls the special method __iter__() on your object to get an iterator. Then it repeatedly calls next(iterator) until that call raises StopIteration. This is how loops actually move through data.Seeing under the hood
# Let's manually iterate over a list
my_list = [10, 20, 30]
iterator = iter(my_list)
print(next(iterator)) # Prints: 10
print(next(iterator)) # Prints: 20
print(next(iterator)) # Prints: 30
try:
print(next(iterator)) # nothing left: raises StopIteration
except StopIteration:
print('Done')To make your own class work in a
for loop, you define two methods. The collection's __iter__() returns an iterator object. That iterator must have a __next__() method that returns the next value or raises StopIteration when it runs out.The Countdown example
class CountDown:
def __init__(self, start):
self.start = start
def __iter__(self):
# Return a fresh iterator each time we loop
return CountdownIterator(self.start)
class CountdownIterator:
def __init__(self, current):
self.current = current
def __iter__(self):
return self # an iterator is iterable too
def __next__(self):
if self.current < 1:
raise StopIteration
val = self.current
self.current -= 1
return val
countdown = CountDown(3)
print(list(countdown)) # [3, 2, 1]
print(list(countdown)) # [3, 2, 1] again: a fresh iterator each timeWhy separate classes? An iterator is used up after one pass. If
__iter__ returned the same object every time, you couldn't loop over it twice. By creating a new iterator instance inside __iter__, each for loop gets its own independent state.