# 6.1. Iterator¶

## 6.1.1. Rationale¶

• Used for iterating in a for loop

## 6.1.2. Protocol¶

• __iter__(self) -> self

• __next__(self) -> raise StopIteration

• iter(obj) -> obj.__iter__()

• next(obj) -> obj.__next__()

class Iterator:
def __iter__(self):
self._current = 0
return self

def __next__(self):
if self._current >= len(self.values):
raise StopIteration

element = self.values[self._current]
self._current += 1
return element


## 6.1.3. Example¶

class Crew:
def __init__(self):
self.members = list()

self.members.append(other)
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

crew = Crew()
crew += 'Mark Watney'
crew += 'Jose Jimenez'
crew += 'Melissa Lewis'

for member in crew:
print(member)

# Mark Watney
# Jose Jimenez
# Melissa Lewis


## 6.1.4. Loop and Iterators¶

Listing 6.63. For loop
DATA = [1, 2, 3]

for current in DATA:
print(current)

Listing 6.64. Intuitive implementation of the for loop
DATA = [1, 2, 3]
iterator = iter(DATA)

try:
current = next(iterator)
print(current)

current = next(iterator)
print(current)

current = next(iterator)
print(current)

current = next(iterator)
print(current)
except StopIteration:
pass

Listing 6.65. Intuitive implementation of the for loop
DATA = [1, 2, 3]
iterator = DATA.__iter__()

try:
current = iterator.__next__()
print(current)

current = iterator.__next__()
print(current)

current = iterator.__next__()
print(current)

current = iterator.__next__()
print(current)
except StopIteration:
pass


## 6.1.5. Built-in Type Iteration¶

Listing 6.66. Iterating str
for character in 'hello':
print(character)

# h
# e
# l
# l
# o

Listing 6.67. Iterating sequences
for number in [1, 2, 3]:
print(number)

# 1
# 2
# 3

Listing 6.68. Iterating dict
DATA = {'a': 1, 'b': 2, 'c': 3}

for element in DATA:
print(element)

# a
# b
# c

Listing 6.69. Iterating dict
for key, value in DATA.items():
print(f'{key} -> {value}')

# a -> 1
# b -> 2
# c -> 3

Listing 6.70. Iterating nested sequences
for key, value in [('a',1), ('b',2), ('c',3)]:
print(f'{key} -> {value}')

# a -> 1
# b -> 2
# c -> 3


## 6.1.6. Use Cases¶

Listing 6.71. Iterator implementation
class Parking:
def __init__(self):
self._parked_cars = list()

def park(self, car):
self._parked_cars.append(car)

def __iter__(self):
self._current = 0
return self

def __next__(self):
if self._current >= len(self._parked_cars):
raise StopIteration

element = self._parked_cars[self._current]
self._current += 1
return element

parking = Parking()
parking.park('Mercedes')
parking.park('Maluch')
parking.park('Toyota')

for car in parking:
print(car)

# Mercedes
# Maluch
# Toyota


## 6.1.7. Standard Library Itertools¶

• import itertools

Listing 6.72. itertools.count(start=0, step=1)
from itertools import count

data = count(3, 2)

next(data)
# 3

next(data)
# 5

next(data)
# 7

Listing 6.73. itertools.cycle(iterable)
from itertools import cycle

DATA = ['white', 'gray']

for color in cycle(DATA):
print(color)

# white
# gray
# white
# gray
# ...

Listing 6.74. itertools.cycle(iterable)
from itertools import cycle

DATA = ['even', 'odd']

for i, status in enumerate(cycle(DATA)):
print(i, status)

# 0, even
# 1, odd
# 2, even
# 3, odd
# ...

Listing 6.75. itertools.repeat(object[, times])
from itertools import repeat

data = repeat(10, 3)
data
# repeat(10, 3)

next(data)
# 10

next(data)
# 10

next(data)
# 10

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

Listing 6.76. itertools.accumulate(iterable[, func, *, initial=None])
from itertools import accumulate

data = accumulate([1, 2, 3, 4])

next(data)
# 1

next(data)
# 3

next(data)
# 6

next(data)
# 10

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

Listing 6.77. itertools.chain(*iterables)
from itertools import chain

keys = ['a', 'b', 'c']
values = [1, 2, 3]

for x in chain(keys, values):
print(x)

# a
# b
# c
# 1
# 2
# 3

Listing 6.78. itertools.chain(*iterables)
from itertools import chain

class Iterator:
def __iter__(self):
self._current = 0
return self

def __next__(self):
if self._current >= len(self.values):
raise StopIteration

element = self.values[self._current]
self._current += 1
return element

class Character(Iterator):
def __init__(self, *values):
self.values = values

class Number(Iterator):
def __init__(self, *values):
self.values = values

chars = Character('a', 'b', 'c')
nums = Number(1, 2, 3)

print(chain(chars, nums))
# <itertools.chain object at 0x116166970>

print(list(chain(chars, nums)))
# [1, 2, 3, 'a', 'b', 'c']

for x in chain(chars, nums):
print(x)

# a
# b
# c
# 1
# 2
# 3

Listing 6.79. itertools.compress(data, selectors)
from itertools import compress

data = compress('ABCDEF', [1,0,1,0,1,1])

next(data)
# 'A'

next(data)
# 'C'

next(data)
# 'E'

next(data)
# 'F'

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

Listing 6.80. itertools.islice(iterable, start, stop[, step])
from itertools import islice

data = islice('ABCDEFG', 2, 6, 2 )

next(data)
# 'C'

next(data)
# 'E'

next(data)
# Traceback (most recent call last):
#   File "<input>", line 1, in <module>
# StopIteration

Listing 6.81. itertools.starmap(function, iterable)
from itertools import starmap

data = starmap(pow, [(2,5), (3,2), (10,3)])

next(data)
# 32

next(data)
# 9

next(data)
# 1000

next(data)
# Traceback (most recent call last):
#   File "<input>", line 1, in <module>
# StopIteration

Listing 6.82. itertools.product(*iterables, repeat=1)
from itertools import product

data = product(['a', 'b', 'c'], [1,2])

next(data)
# ('a', 1)

next(data)
# ('a', 2)

next(data)
# ('b', 1)

next(data)
# ('b', 2)

next(data)
# ('c', 1)

next(data)
# ('c', 2)

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

Listing 6.83. itertools.permutations(iterable, r=None)
from itertools import permutations

data = permutations([1,2,3])

next(data)
# (1, 2, 3)

next(data)
# (1, 3, 2)

next(data)
# (2, 1, 3)

next(data)
# (2, 3, 1)

next(data)
# (3, 1, 2)

next(data)
# (3, 2, 1)

next(data)
# Traceback (most recent call last):
#   File "<input>", line 1, in <module>
# StopIteration

Listing 6.84. itertools.combinations(iterable, r)
from itertools import combinations

data = combinations([1, 2, 3, 4], 2)

next(data)
# (1, 2)

next(data)
# (1, 3)

next(data)
# (1, 4)

next(data)
# (2, 3)

next(data)
# (2, 4)

next(data)
# (3, 4)

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

Listing 6.85. itertools.combinations_with_replacement(iterable, r)
from itertools import combinations_with_replacement

data = combinations_with_replacement([1,2,3], 2)

next(data)
# (1, 1)

next(data)
# (1, 2)

next(data)
# (1, 3)

next(data)
# (2, 2)

next(data)
# (2, 3)

next(data)
# (3, 3)

next(data)
# Traceback (most recent call last):
#   File "<input>", line 1, in <module>
# StopIteration

Listing 6.86. itertools.groupby(iterable, key=None). Make an iterator that returns consecutive keys and groups from the iterable. Generally, the iterable needs to already be sorted on the same key function. The operation of groupby() is similar to the uniq filter in Unix. It generates a break or new group every time the value of the key function changes. That behavior differs from SQL’s GROUP BY which aggregates common elements regardless of their input order.
from itertools import groupby

data = groupby('AAAABBBCCDAABBB')

next(data)
# ('A', <itertools._grouper object at 0x1215f5c70>)

next(data)
# ('B', <itertools._grouper object at 0x12157b4f0>)

next(data)
# ('C', <itertools._grouper object at 0x120e16ee0>)

next(data)
# ('D', <itertools._grouper object at 0x1215ef4c0>)

next(data)
# ('A', <itertools._grouper object at 0x12157b3a0>)

next(data)
# ('B', <itertools._grouper object at 0x12157b790>)

next(data)
# Traceback (most recent call last):
#   ...
# StopIteration

[k for k, g in groupby('AAAABBBCCDAABBB')]
# A B C D A B

[list(g) for k, g in groupby('AAAABBBCCD')]
# AAAA BBB CC D


## 6.1.8. Assignments¶

### 6.1.8.1. Protocol Iterator Implementation¶

• Assignment name: Protocol Iterator Implementation

• Last update: 2020-10-02

• Complexity level: easy

• Lines of code to write: 9 lines

• Estimated time of completion: 8 min

English
1. Use data from "Input" section (see below)

2. Modify classes to implement iterator protocol

3. Iterator should return instances of Mission

4. All tests must pass

5. Compare result with "Output" section (see below)

Polish
1. Użyj danych z sekcji "Input" (patrz poniżej)

2. Zmodyfikuj klasy aby zaimplementować protokół iterator

3. Iterator powinien zwracać instancje Mission

4. Wszystkie testy muszą przejść

5. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
from dataclasses import dataclass

@dataclass
class Astronaut:
firstname: str
lastname: str
missions: tuple = ()

@dataclass
class Mission:
year: int
name: str

Output
>>> from inspect import isclass, ismethod
>>> assert isclass(Astronaut)

>>> astro = Astronaut('Mark', 'Watney')
>>> assert hasattr(astro, 'firstname')
>>> assert hasattr(astro, 'lastname')
>>> assert hasattr(astro, 'missions')
>>> assert hasattr(astro, '__iter__')
>>> assert hasattr(astro, '__next__')
>>> assert ismethod(astro.__iter__)
>>> assert ismethod(astro.__next__)

>>> astro = Astronaut('Jan', 'Twardowski', missions=(
...     Mission(1969, 'Apollo 11'),
...     Mission(2024, 'Artemis 3'),
...     Mission(2035, 'Ares 3'),
... ))

>>> for mission in astro:
...     print(mission)
Mission(year=1969, name='Apollo 11')
Mission(year=2024, name='Artemis 3')
Mission(year=2035, name='Ares 3')


### 6.1.8.2. Protocol Iterator Range¶

• Assignment name: Protocol Iterator Range

• Last update: 2020-10-02

• Complexity level: medium

• Lines of code to write: 25 lines

• Estimated time of completion: 21 min

English
1. Use data from "Input" section (see below)

2. Define class Range with parameters: start, stop, step

3. Write own implementation of a built-in range(start, stop, step) function

4. Use Iterator protocol

5. How to implement passing only stop argument (range(start=0, stop=???, step=1))?

6. All tests must pass

7. Compare result with "Output" section (see below)

Polish
1. Użyj danych z sekcji "Input" (patrz poniżej)

2. Zdefiniuj klasę Range z parametrami: start, stop, step

3. Zaimplementuj własne rozwiązanie wbudowanej funkcji range(start, stop, step)

4. Użyj protokołu Iterator

5. Jak zaimplementować możliwość podawania tylko końca (range(start=0, stop=???, step=1))?

6. Wszystkie testy muszą przejść

7. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Output
>>> 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]

>>> list(Range(5))
[0, 1, 2, 3, 4]

>>> list(Range())
Traceback (most recent call last):
...
ValueError: Invalid arguments

>>> list(Range(1,2,3,4))
Traceback (most recent call last):
...
ValueError: Invalid arguments

>>> Range(stop=2)
Traceback (most recent call last):
...
TypeError: Range() takes no keyword arguments

>>> Range(start=1, stop=2)
Traceback (most recent call last):
...
TypeError: Range() takes no keyword arguments

>>> Range(start=1, stop=2, step=2)
Traceback (most recent call last):
...
TypeError: Range() takes no keyword arguments