0%

Iteration as an Interface · practice

Make a Domain Object Iterable

Chapter 6 gave Quiz a to hand out its questions:

for question in quiz.questions():
    print(question.prompt)

That remains useful when a caller needs a snapshot. When callers only need to , the can offer iteration directly:

for question in quiz:
    print(question.prompt)

Delegating to something already iterable

Quiz holds a list, and a list already knows how to be walked. So __iter__ has nothing to invent:

Try it

One line, and no __next__ anywhere. iter(self._questions) asks the list for a fresh and hands it back, so the quiz is re- for free: each pass gets its own walker, for the reasons Lesson 3 spelled out.

This is what you write in practice. The two-class version from Lesson 3 exists so you can see what iter() on that list is doing; almost every real __iter__ is this one line.

What it buys

The gain is not that for question in quiz is three characters shorter. It is that Quiz now works with everything that speaks the iteration protocol, without any of it knowing what a Quiz is:

Try it

sorted, in, enumerate, zip, min, max, sum, any, all, and unpacking can all consume an iterable. None of them were written with Quiz in mind. They ask for an iterator, and Quiz answers. The items still need to suit the operation: this quiz contains , for example, so you can sort its questions but cannot add them with sum.

This is the chapter’s title. Iteration is an interface: implement one method and a large part of the language starts accepting your object.

Why can a Quiz whose __iter__ returns iter(self._questions) be looped over twice, when the Countdown from the previous lesson could not?

Handing out a copy, revisited

Chapter 6 was careful to return list(self._questions) so a caller could not empty the quiz through the list it was given. Does __iter__ reopen that hole?

Not through the iterator itself. An iterator lets a caller receive items one at a time, and the protocol offers no operation for adding or removing entries from the bank. The yielded objects may still be , so for question in quiz protects the collection’s structure rather than promising deep read-only access.

That makes __iter__ a better answer here than questions(): it exposes one natural stream of questions without handing out a list to measure, , or modify. Keep a questions() method only if a caller genuinely needs that sequence-like interface.

Choosing what to iterate

An object with several collections has to decide which one for x in obj means, and the answer should be the obvious one. A Quiz iterates its questions. A Gradebook iterates its records. If there is no obvious choice, do not define __iter__ at all: give the collections names, and let callers write for result in book.records().

__iter__ promises a traversal of one particular kind of item. It does not promise indexing or a length; those are separate sequence operations.

Task

Make both classes by delegating to the collection each already holds.

Quiz.__iter__ walks its questions. Gradebook.__iter__ walks its records. Each is one line, and neither needs a __next__.

Both must stay re-iterable: looping twice sees everything twice.

Then write two that work through the protocol rather than through a name:

  • total_points(quiz) sums the points of every question, using the quiz directly;

  • top_record(book) returns the record with the highest percentage, or None when the gradebook is empty.

Neither may call .questions() or .records(). The grader passes them an ordinary as well as a Quiz and a Gradebook, which only works if they ask for and nothing else.