Proving the Cursor Is Lazy

Step 4: Proving the Cursor Is Lazy

The payoff of the Iterator pattern is that work happens only when items are actually pulled. A well-built Cursor never touches the backend until you ask for the first item, and it fetches exactly the pages it needs, no more.

PagedSource counts every call in self.fetches, so we can prove it. Add take(n) to Cursor (same idea as ListCursor.take) so we can consume a bounded number of items, then watch the counter: constructing a Cursor should fetch zero pages, taking one item should fetch one, and reaching across a page boundary should fetch exactly two.

Hints
Starter code
class ListCursor:
    def __init__(self, items):
        self._items = list(items)
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index >= len(self._items):
            raise StopIteration
        value = self._items[self._index]
        self._index += 1
        return value

    def take(self, n):
        result = []
        for _ in range(n):
            try:
                result.append(next(self))
            except StopIteration:
                break
        return result


class PagedSource:
    def __init__(self, items, page_size):
        self.items = list(items)
        self.page_size = page_size
        self.fetches = 0

    def page(self, index):
        self.fetches += 1
        start = index * self.page_size
        return self.items[start:start + self.page_size]

class Cursor:
    def __init__(self, source):
        self.source = source
        self._page_index = 0
        self._buffer = []
        self._pos = 0
        self._done = False

    def __iter__(self):
        return self

    def __next__(self):
        while self._pos >= len(self._buffer):
            if self._done:
                raise StopIteration
            page = self.source.page(self._page_index)
            self._page_index += 1
            if not page:
                self._done = True
                raise StopIteration
            self._buffer = page
            self._pos = 0
        value = self._buffer[self._pos]
        self._pos += 1
        return value

    def take(self, n):
        """Pull up to n items from the paged cursor without fetching extra pages."""
        # TODO: the same bounded pull as ListCursor.take, only now every value
        # comes through the paging __next__ above. Ask for at most n items,
        # gather them into a list, and stop early once the cursor reports it is
        # exhausted. Never look ahead: source.fetches must rise only when the
        # buffer is genuinely empty and an item has actually been requested.
        pass
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.