8.1. Iterator
EN: Iterator
PL: Iterator
Type: object
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.
Here's a simple example of an Iterator in Python:
>>> class MyIterator:
... def __init__(self, data):
... self.data = data
...
... def __iter__(self):
... self._current = 0
... return self
...
... def __next__(self):
... if self._current >= len(self.data):
... raise StopIteration
... result = self.data[self._current]
... self._current += 1
... return result
This will output:
>>> my_data = [1, 2, 3, 4, 5]
>>> my_iterator = MyIterator(my_data)
>>> for item in my_iterator:
... print(item)
...
1
2
3
4
5
8.1.1. Pattern
History (like browser history)
8.1.2. 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
8.1.3. 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
8.1.4. Use Case - 1
from urllib.request import urlopen
from dataclasses import dataclass, field
from typing import Self
@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
8.1.5. 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
8.1.6. 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
8.1.7. Assignments
# %% 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
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: DesignPatterns Behavioral Iterator
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5
# %% English
# 1. Implement Iterator pattern
# 2. Run doctests - all must succeed
# %% Polish
# 1. Zaimplementuj wzorzec Iterator
# 2. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> crew = Crew()
>>> crew += 'Mark Watney'
>>> crew += 'Jose Jimenez'
>>> crew += 'Melissa Lewis'
>>>
>>> for member in crew:
... print(member)
Mark Watney
Jose Jimenez
Melissa Lewis
"""
class Crew:
def __init__(self):
self.members = list()
def __iadd__(self, other):
self.members.append(other)
return self
# %% 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
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Protocol Iterator Implementation
# - Difficulty: easy
# - Lines: 9
# - Minutes: 3
# %% 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ść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isclass, ismethod
>>> assert isclass(User)
>>> mark = User('Mark', 'Watney')
>>> assert hasattr(mark, 'firstname')
>>> assert hasattr(mark, 'lastname')
>>> assert hasattr(mark, 'groups')
>>> assert hasattr(mark, '__iter__')
>>> assert hasattr(mark, '__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')
"""
from dataclasses import dataclass
@dataclass
class User:
firstname: str
lastname: str
groups: tuple = ()
@dataclass
class Group:
gid: int
name: str
# %% 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
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Protocol Iterator Range
# - Difficulty: medium
# - Lines: 9
# - Minutes: 8
# %% 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ść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isclass, ismethod
>>> assert isclass(Range)
>>> r = Range(0, 0, 0)
>>> assert hasattr(r, '__iter__')
>>> assert hasattr(r, '__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]
"""
from dataclasses import dataclass
@dataclass
class Range:
start: int = 0
stop: int = None
step: int = 1