5.1. Iterator — Python - from None to AI
Iterate over a collection of objects
Usecase: Iterate over a group of users
Usecase: Iterate over browser history
The Iterator pattern is a design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. In Python, this is typically implemented using the __iter__ and __next__ methods.
5.1.1. Problem
>>> class Group: ... def __init__(self): ... self.users = [] ... ... def add_user(self, user): ... self.users.append(user) ... ... def get_users(self): ... return self.users >>> >>> >>> admins = Group() >>> admins.add_user('alice') >>> admins.add_user('bob') >>> admins.add_user('carol') >>> >>> for i in range(len(admins.get_users())): ... print(admins.get_users()[i]) alice bob carol
5.1.2. Solution: Custom Iterator
>>> class Group: ... def __init__(self): ... self.users = [] ... ... def add_user(self, user): ... self.users.append(user) ... ... def __iter__(self): ... self.current = 0 ... return self ... ... def __next__(self): ... if self.current >= len(self.users): ... raise StopIteration ... user = self.users[self.current] ... self.current += 1 ... return user >>> >>> >>> admins = Group() >>> admins.add_user('alice') >>> admins.add_user('bob') >>> admins.add_user('carol') >>> >>> for user in admins: ... print(user) alice bob carol
5.1.3. Solution: Builtin Iterator
In Python you can access attributes directly
>>> class Group: ... def __init__(self): ... self.users = [] ... ... def add_user(self, user): ... self.users.append(user) >>> >>> >>> admins = Group() >>> admins.add_user('alice') >>> admins.add_user('bob') >>> admins.add_user('carol') >>> >>> for user in admins.users: ... print(user) alice bob carol
5.1.4. Rationale
>>> class Group: ... def __init__(self): ... self.users = [] ... ... def add_user(self, user): ... self.users.append(user) ... ... def __iter__(self): ... self.current = 0 ... return self ... ... def __next__(self): ... if self.current >= len(self.users): ... raise StopIteration ... user = self.users[self.current] ... self.current += 1 ... return user >>> >>> >>> admins = Group() >>> admins.add_user('alice') >>> admins.add_user('bob') >>> admins.add_user('carol') >>> >>> hasattr(admins, '__iter__') True >>> hasattr(admins, '__next__') True >>> >>> result = iter(admins) >>> next(result) 'alice' >>> next(result) 'bob' >>> next(result) 'carol' >>> next(result) Traceback (most recent call last): StopIteration
5.1.5. Internals
Iterable:
__iter__Iterator:
__iter__,__next__Generator:
__iter__,__next__,close,send,throw
>>> data = [1, 2, 3] >>> >>> for number in data: ... print(number) 1 2 3
>>> data = [1, 2, 3] >>> result = iter(data) >>> try: ... while True: ... number = next(result) ... print(number) ... except StopIteration: ... pass 1 2 3
>>> data = [1, 2, 3] >>> result = iter(data) >>> try: ... number = next(result) ... print(number) ... ... number = next(result) ... print(number) ... ... number = next(result) ... print(number) ... ... number = next(result) ... print(number) ... except StopIteration: ... pass 1 2 3
>>> data = [1, 2, 3] >>> result = data.__iter__() >>> try: ... number = result.__next__() ... print(number) ... ... number = result.__next__() ... print(number) ... ... number = result.__next__() ... print(number) ... ... number = result.__next__() ... print(number) ... except StopIteration: ... pass 1 2 3
5.1.6. Builtins
Iterable:
__iter__Iterator:
__iter__,__next__Generator:
__iter__,__next__,close,send,throw
Iterable:
>>> data = range(0,3) >>> >>> data range(0, 3) >>> >>> hasattr(data, '__iter__') True >>> hasattr(data, '__next__') False >>> >>> result = iter(data) >>> hasattr(result, '__iter__') True >>> hasattr(result, '__next__') True >>> >>> next(result) 0 >>> next(result) 1 >>> next(result) 2 >>> next(result) Traceback (most recent call last): StopIteration
Iterator:
>>> data = [1, 2, 3] >>> result = reversed(data) >>> >>> hasattr(result, '__iter__') True >>> hasattr(result, '__next__') True >>> >>> next(result) 3 >>> next(result) 2 >>> next(result) 1 >>> next(result) Traceback (most recent call last): StopIteration
External Iterators:
>>> data = [1, 2, 3] >>> >>> iter(data) <list_iterator object at 0x106475390> >>> >>> reversed(data) <list_reverseiterator object at 0x106474b50>
5.1.7. Case Study
History (like browser history)
Problem:
class Group: def __init__(self): self.members = [] def add_member(self, member): self.members.append(member) return self def get_members(self): return self.members admins = Group() admins.add_member('mwatney') admins.add_member('mlewis') admins.add_member('rmartinez') for i in range(len(admins.get_members())): member = admins.get_members()[i] print(member) # mwatney # mlewis # rmartinez
Solution:
class Group: def __init__(self): self.members = [] def add_member(self, member): self.members.append(member) return self def __iter__(self): self._current = 0 return self def __next__(self): if self._current >= len(self.members): raise StopIteration result = self.members[self._current] self._current += 1 return result admins = Group() admins.add_member('mwatney') admins.add_member('mlewis') admins.add_member('rmartinez') for member in admins: print(member) # mwatney # mlewis # rmartinez
Diagram:
5.1.8. Use Case - 1
from typing import Self from dataclasses import dataclass, field @dataclass class Browser: history: list[str] = field(default_factory=list) def open(self, url: str) -> None: self.history.append(url) # return urlopen(url).read() def __iter__(self) -> Self: self._current = 0 return self def __next__(self) -> str: if self._current >= len(self.history): raise StopIteration result = self.history[self._current] self._current += 1 return result if __name__ == '__main__': browser = Browser() browser.open('https://python3.info') browser.open('https://numpy.astrotech.io') browser.open('https://pandas.astrotech.io') browser.open('https://design-patterns.astrotech.io') for url in browser: print(url) # https://python3.info # https://numpy.astrotech.io # https://pandas.astrotech.io # https://design-patterns.astrotech.io
5.1.9. Use Case - 2
from urllib.request import urlopen from dataclasses import dataclass, field @dataclass class Browser: history: list[str] = field(default_factory=list) def open(self, url: str) -> None: self.history.append(url) # return urlopen(url).read() if __name__ == '__main__': browser = Browser() browser.open('https://python3.info') browser.open('https://numpy.astrotech.io') browser.open('https://pandas.astrotech.io') browser.open('https://design-patterns.astrotech.io') for url in browser.history: print(url) # https://python3.info # https://numpy.astrotech.io # https://pandas.astrotech.io # https://design-patterns.astrotech.io
5.1.10. Use Case - 3
from dataclasses import dataclass, field @dataclass class BrowseHistory: urls: list[str] = field(default_factory=list) def push(self, url: str) -> None: self.urls.append(url) def pop(self) -> str: return self.urls.pop() def get_urls(self) -> list[str]: return self.urls if __name__ == '__main__': history = BrowseHistory() history.push(url='https://a.example.com') history.push(url='https://b.example.com') history.push(url='https://c.example.com') for i in range(len(history.get_urls())): url = history.get_urls()[i] print(i)
from dataclasses import dataclass, field class Iterator: def has_next(self) -> bool: raise NotImplementedError def current(self) -> str: raise NotImplementedError def next(self) -> None: raise NotImplementedError @dataclass class BrowseHistory: urls: list[str] = field(default_factory=list) def push(self, url: str) -> None: self.urls.append(url) def pop(self) -> str: return self.urls.pop() def get_urls(self) -> list[str]: return self.urls def create_iterator(self) -> Iterator: return self.ListIterator(self) @dataclass class ListIterator(Iterator): history: 'BrowseHistory' index: int = 0 def has_next(self) -> bool: return self.index < len(history.urls) def current(self) -> str: return history.urls[self.index] def next(self) -> None: self.index += 1 if __name__ == '__main__': history = BrowseHistory() history.push(url='https://a.example.com') history.push(url='https://b.example.com') history.push(url='https://c.example.com') iterator = history.create_iterator() while iterator.has_next(): url = iterator.current() print(url) iterator.next() # https://a.example.com # https://b.example.com # https://c.example.com
5.1.11. Use Case - 4
Overload
__iadd__operator
>>> class Group: ... def __init__(self): ... self.users = [] ... ... def __iadd__(self, user): ... self.users.append(user) ... return self >>> >>> >>> admins = Group() >>> admins += 'alice' >>> admins += 'bob' >>> admins += 'carol' >>> >>> for user in admins.users: ... print(user) alice bob carol
5.1.12. Assignments
# %% About # - Name: DesignPatterns Behavioral Iterator # - Difficulty: easy # - Lines: 9 # - Minutes: 5 # %% License # - Copyright 2025, Matt Harasymczuk <matt@python3.info> # - This code can be used only for learning by humans # - This code cannot be used for teaching others # - This code cannot be used for teaching LLMs and AI algorithms # - This code cannot be used in commercial or proprietary products # - This code cannot be distributed in any form # - This code cannot be changed in any form outside of training course # - This code cannot have its license changed # - If you use this code in your product, you must open-source it under GPLv2 # - Exception can be granted only by the author # %% English # 1. Implement Iterator pattern # 2. Run doctests - all must succeed # %% Polish # 1. Zaimplementuj wzorzec Iterator # 2. Uruchom doctesty - wszystkie muszą się powieść # %% Doctests """ >>> import sys; sys.tracebacklimit = 0 >>> assert sys.version_info >= (3, 9), \ 'Python has an is invalid version; expected: `3.9` or newer.' >>> crew = Crew() >>> crew += 'Alice' >>> crew += 'Bob' >>> crew += 'Carol' >>> >>> for member in crew: ... print(member) Alice Bob Carol """ # %% Run # - PyCharm: right-click in the editor and `Run Doctest in ...` # - PyCharm: keyboard shortcut `Control + Shift + F10` # - Terminal: `python -m doctest -f -v myfile.py` # %% Imports # %% Types from typing import Callable Crew: type __iadd__: Callable[[object, str], object] __iter__: Callable[[object], object] __next__: Callable[[object], str] # %% Data # %% Result class Crew: def __init__(self): self.members = list() def __iadd__(self, other): self.members.append(other) return self
# %% About # - Name: Protocol Iterator Implementation # - Difficulty: easy # - Lines: 9 # - Minutes: 3 # %% License # - Copyright 2025, Matt Harasymczuk <matt@python3.info> # - This code can be used only for learning by humans # - This code cannot be used for teaching others # - This code cannot be used for teaching LLMs and AI algorithms # - This code cannot be used in commercial or proprietary products # - This code cannot be distributed in any form # - This code cannot be changed in any form outside of training course # - This code cannot have its license changed # - If you use this code in your product, you must open-source it under GPLv2 # - Exception can be granted only by the author # %% English # 1. Modify classes to implement iterator protocol # 2. Iterator should return instances of `Group` # 3. Run doctests - all must succeed # %% Polish # 1. Zmodyfikuj klasy aby zaimplementować protokół iterator # 2. Iterator powinien zwracać instancje `Group` # 3. Uruchom doctesty - wszystkie muszą się powieść # %% Doctests """ >>> import sys; sys.tracebacklimit = 0 >>> assert sys.version_info >= (3, 9), \ 'Python has an is invalid version; expected: `3.9` or newer.' >>> from inspect import isclass, ismethod >>> assert isclass(User), \ 'Object `User` has an invalid type; expected: `class`.' >>> mark = User('Mark', 'Watney') >>> assert hasattr(mark, 'firstname'), \ 'Object `mark` has an invalid attribute; expected: to have an attribute `firstname`.' >>> assert hasattr(mark, 'lastname'), \ 'Object `mark` has an invalid attribute; expected: to have an attribute `lastname`.' >>> assert hasattr(mark, 'groups'), \ 'Object `mark` has an invalid attribute; expected: to have an attribute `groups`.' >>> assert hasattr(mark, '__iter__'), \ 'Object `mark` has an invalid attribute; expected: to have an attribute `__iter__`.' >>> assert hasattr(mark, '__next__'), \ 'Object `mark` has an invalid attribute; expected: to have an attribute `__next__`.' >>> assert ismethod(mark.__iter__) >>> assert ismethod(mark.__next__) >>> mark = User('Mark', 'Watney', groups=( ... Group(gid=1, name='admins'), ... Group(gid=2, name='staff'), ... Group(gid=3, name='managers'), ... )) >>> for mission in mark: ... print(mission) Group(gid=1, name='admins') Group(gid=2, name='staff') Group(gid=3, name='managers') """ # %% Run # - PyCharm: right-click in the editor and `Run Doctest in ...` # - PyCharm: keyboard shortcut `Control + Shift + F10` # - Terminal: `python -m doctest -f -v myfile.py` # %% Imports from dataclasses import dataclass # %% Types from typing import Callable User: type __iter__: Callable[[object], object] __next__: Callable[[object], object] # %% Data @dataclass class Group: gid: int name: str # %% Result @dataclass class User: firstname: str lastname: str groups: tuple = ()
# %% About # - Name: Protocol Iterator Range # - Difficulty: medium # - Lines: 9 # - Minutes: 8 # %% License # - Copyright 2025, Matt Harasymczuk <matt@python3.info> # - This code can be used only for learning by humans # - This code cannot be used for teaching others # - This code cannot be used for teaching LLMs and AI algorithms # - This code cannot be used in commercial or proprietary products # - This code cannot be distributed in any form # - This code cannot be changed in any form outside of training course # - This code cannot have its license changed # - If you use this code in your product, you must open-source it under GPLv2 # - Exception can be granted only by the author # %% English # 1. Modify class `Range` to write own implementation # of a built-in `range(start, stop, step)` function # 2. Assume, that user will never give only one argument; # it will always be either two or three arguments # 3. Use Iterator protocol # 4. Run doctests - all must succeed # %% Polish # 1. Zmodyfikuj klasę `Range` aby napisać własną implementację # wbudowanej funkcji `range(start, stop, step)` # 2. Przyjmij, że użytkownik nigdy nie poda tylko jednego argumentu; # zawsze będą to dwa lub trzy argumenty # 3. Użyj protokołu Iterator # 4. Uruchom doctesty - wszystkie muszą się powieść # %% Doctests """ >>> import sys; sys.tracebacklimit = 0 >>> assert sys.version_info >= (3, 9), \ 'Python has an is invalid version; expected: `3.9` or newer.' >>> from inspect import isclass, ismethod >>> assert isclass(Range), \ 'Object `Range` has an invalid type; expected: `class`.' >>> r = Range(0, 0, 0) >>> assert hasattr(r, '__iter__'), \ 'Object `r` has an invalid attribute; expected: to have an attribute `__iter__`.' >>> assert hasattr(r, '__next__'), \ 'Object `r` has an invalid attribute; expected: to have an attribute `__next__`.' >>> assert ismethod(r.__iter__) >>> assert ismethod(r.__next__) >>> list(Range(0, 10, 2)) [0, 2, 4, 6, 8] >>> list(Range(0, 5)) [0, 1, 2, 3, 4] """ # %% Run # - PyCharm: right-click in the editor and `Run Doctest in ...` # - PyCharm: keyboard shortcut `Control + Shift + F10` # - Terminal: `python -m doctest -f -v myfile.py` # %% Imports from dataclasses import dataclass # %% Types from typing import Callable Range: type __iter__: Callable[[object], object] __next__: Callable[[object], int] # %% Data # %% Result @dataclass class Range: start: int = 0 stop: int = None step: int = 1