Expert · Advanced Python · topic 40 of 50 · 30 min
Iterators and generators
Explanation
Two words that sound alike: iterable and iterator
An iterable is anything you can use in a for loop: a list, a string, a tuple, a dict, a set, a range, an open file. An iterator is a different, smaller object. Its only job is to give you the next item each time you ask, and to remember how far it has reached.
iter() and next()
The built-in function iter() asks an iterable for a fresh iterator. The built-in function next() asks an iterator for its next item. When nothing is left, next() raises a special exception called StopIteration. That exception is not a bug; it is the signal for "the end".
marks = [72, 88, 95]
it = iter(marks) # get an iterator from the list
print(next(it)) # 72
print(next(it)) # 88
print(next(it)) # 95
try:
next(it) # nothing left
except StopIteration:
print("No more items")A for loop is a short way of writing the steps above. Python calls iter() on your iterable once, then calls next() again and again, and stops quietly when it sees StopIteration. Knowing this explains a lot of Python behaviour that looks like magic.
cities = ["Pune", "Jaipur", "Kochi"]
it = iter(cities)
while True:
try:
city = next(it)
except StopIteration:
break
print(city)A protocol is an agreed set of method names. Any object becomes an iterator if it has two dunder methods: __iter__, which returns the iterator itself, and __next__, which returns the next item or raises StopIteration. You rarely write these by hand, because generators (below) do it for you.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3))) # [3, 2, 1]Generator functions and yield
A generator function is a normal def that contains the word yield. Calling it does not run the body. Instead it returns a generator object, which is an iterator. Each next() runs the body until the next yield, hands out that value, and pauses. Local variables are kept safe until the next call.
def countdown(n):
while n > 0:
yield n
n -= 1
gen = countdown(3)
print(gen) # <generator object countdown at ...>
print(next(gen)) # 3
print(list(gen)) # [2, 1] (the rest of the values)Lazy evaluation means a value is worked out only when someone asks for it. A list stores all its items in memory at once. A generator stores only its current position, so it uses the same small memory for ten items or ten million items.
import sys
squares_list = [x * x for x in range(1_000_000)] # list comprehension
squares_gen = (x * x for x in range(1_000_000)) # generator expression
print(sys.getsizeof(squares_list)) # millions of bytes
print(sys.getsizeof(squares_gen)) # around 200 bytes
print(sum(x * x for x in range(1_000_000)))yield from some_iterable yields every item of another iterable, one by one. It is a short form of "for item in some_iterable: yield item", and it is handy when one generator is built from smaller ones.
def all_orders(shop_a, shop_b):
yield from shop_a
yield from shop_b
print(list(all_orders(["dosa", "idli"], ["vada pav"])))| List | Generator | |
|---|---|---|
| Memory | Holds every item | Holds only its current position |
| Reuse | Loop as many times as you like | Used once, then empty |
| Indexing like x[3] and len() | Yes | No |
| Best for | Small data you reuse | Big or endless data, pipelines |
Watch the lesson
Iterators and generators · 1/7
In this lesson you will learn how Python loops really work, and how generators let you handle huge amounts of data with very little memory.
Prefer a video by a teacher? Find free videos on “python iterators and generators yield explained” on YouTube ↗
In real life: Reading a huge railway log one line at a time
A ticket booking server writes millions of lines to a log every day. Loading the whole file into a list could use gigabytes of memory. A generator pipeline reads one line, keeps it only if it is an error, and moves on, so the program runs on a small server. Here a list stands in for the file, but the code is the same for an open file.
log = [
"10:01 OK booking PNR1001",
"10:02 ERROR payment timeout",
"10:03 OK booking PNR1002",
"10:04 ERROR seat lock failed",
]
def errors(lines):
for line in lines:
if "ERROR" in line:
yield line
def messages(lines):
for line in lines:
yield line.split(" ", 2)[2]
for msg in messages(errors(log)):
print(msg)Practice (0/3 done)
Write a generator function countdown(n) that yields n, n-1, ... down to 1. list(countdown(3)) should be [3, 2, 1]. It must use yield (the checker tests that it returns a generator).
Write a generator chunks(items, size) that yields lists of at most size items. chunks([1, 2, 3, 4, 5], 2) should give [1, 2], then [3, 4], then [5]. This is how apps send big data in small batches.
Write take(n, iterable) that returns a LIST of the first n items of any iterable, using iter() and next(). It must also work on an endless iterator such as itertools.count(), so do not convert the whole iterable to a list. If the iterable has fewer than n items, return all of them.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.