Iteration as an Interface · practice
Lazy Expressions and When a List Is Simpler
Chapter 3 introduced the generator
[question.points for question in quiz] # builds a list
(question.points for question in quiz) # builds a generator object
The word for what the second one does is lazy: creating the generator computes none of the point values. It produces each
Watching it be lazy
Nothing is produced until something asks. Compare a
That is the whole difference, and everything else follows from it.
When laziness matters
Stopping early. If you only need the first match, a list comprehension does all the work anyway:
It checked 3, checked 9, found it, and stopped. The list version would have checked all four.
Too much to hold. A million rows in a list is a million rows in memory. Passed lazily, one at a time, it is one row.
Chaining steps. Generators can feed generators, and no step builds a collection:
Each generator names one step without building another collection. The original scores list remains, and list(doubled) collects the final result.
Stop without taking one extra
Stopping after two results should also mean requesting exactly two input items. A for
for item in items:
if len(collected) == 2:
break
collected.append(item)
By the time the
def first_two(items):
taken = 0
for item in items:
yield item
taken = taken + 1
if taken == 2:
return
For a requested count of zero or less, return before entering the loop at all. This is exact bounded consumption: the
next(score for score in scores if score > 7) on [3, 9, 5, 8]. How many scores does the condition test?
When a list is simpler, which is often
Laziness is not free, and the costs are real:
You can only walk it once. The bug from Lesson 1, now easy to write by accident.
It has no length. len() on a generator raises
It is harder to debug. Printing a list shows you the values. Printing a generator shows you that it is a generator.
The work happens somewhere else. An exception raised inside a generator surfaces at the for loop consuming it, which can be a long way from the code that looks responsible.
A short, practical rule:
| Want | Use |
|---|---|
To feed sum, any, all, min, max, or next | a generator expression |
| To stop early on a large or endless source | a generator |
| To return something a caller will index, measure, or reuse | a list |
| To look at it while debugging | a list |
| A handful of items | whichever reads better, and usually a list |
For ten questions, the memory saving is nothing and the readability of a list is worth more than the elegance of laziness.
Returning a generator from a function
If a function returns a generator, say so, because the caller now has a one-shot value with no length:
def passing_records(records, mark):
"""Yield each record reaching the mark.
Returns a generator: walk it once, or wrap it in list() to keep it.
"""
That is the honest version of the trade. The default for a public
Task
Implement preview(items, count). Return a count items from a possibly endless source.
The boundary is exact: taking two items must leave the third item untouched in an existing count is zero or negative, return an empty list without requesting any input.
The supplied first_failing, total_points, high_scorers, and describe