Solutions: You can find the file with solutions for all questions here.

### Question 1: Scale

Implement an iterator class called `ScaleIterator` that scales elements in an iterable `s` by a number `k`.

``````class ScaleIterator:
"""An iterator the scales elements of the iterable s by a number k.

>>> s = ScaleIterator([1, 5, 2], 5)
>>> list(s)
[5, 25, 10]

>>> m = ScaleIterator(naturals(), 2)
>>> [next(m) for _ in range(5)]
[2, 4, 6, 8, 10]
"""
def __init__(self, s, k):
self.s = iter(s)
self.k = k

def __iter__(self):
return self

def __next__(self):
return next(self.s) * self.k``````

Use OK to test your code:

``python3 ok -q ScaleIterator``

### Question 2: Restart

Implement an iterator class called `IteratorRestart` that will reset to the beginning, unlike a normal Iterator that would throw a StopIteration Exception at the end.

``````class IteratorRestart:
"""
>>> iterator = IteratorRestart(2, 7)
>>> for num in iterator:
...     print(num)
2
3
4
5
6
7
>>> for num in iterator:
...     print(num)
2
3
4
5
6
7
"""
def __init__(self, start, end):
self.start = start
self.end = end
self.current = start

def __next__(self):
if self.current > self.end:
raise StopIteration
self.current += 1
return self.current - 1

def __iter__(self):
self.current = self.start
return self``````

Use OK to test your code:

``python3 ok -q IteratorRestart``

### Question 3: Scale

Implement the generator function `scale(s, k)`, which yields elements of the given iterable `s`, scaled by `k`.

``````def scale(s, k):
"""Yield elements of the iterable s scaled by a number k.

>>> s = scale([1, 5, 2], 5)
>>> type(s)
<class 'generator'>
>>> list(s)
[5, 25, 10]

>>> m = scale(naturals(), 2)
>>> [next(m) for _ in range(5)]
[2, 4, 6, 8, 10]
"""
for elem in s:
yield elem * k``````

Use OK to test your code:

``python3 ok -q scale``

### Question 4: Remainder generator

Like functions, generators can also be higher-order. For this problem, we will be writing `remainders_generator`, which yields a series of generator objects.

`remainders_generator` takes in an integer `m`, and yields `m` different generators. The first generator is a generator of multiples of `m`, i.e. numbers where the remainder is 0. The second, a generator of natural numbers with remainder 1 when divided by `m`. The last generator yield natural numbers with remainder `m - 1` when divided by `m`.

``````def remainders_generator(m):
"""
Takes in an integer m, and yields m different remainder groups
of m.

>>> remainders_mod_four = remainders_generator(4)
>>> for rem_group in remainders_mod_four:
...     for _ in range(3):
...         print(next(rem_group))
0
4
8
1
5
9
2
6
10
3
7
11
"""
def remainder_group(rem):
start = rem
while True:
yield start
start += m

for rem in range(m):
yield remainder_group(rem)``````

Note that if you have implemented this correctly, each of the generators yielded by `remainder_generator` will be infinite - you can keep calling `next` on them forever without running into a `StopIteration` exception.

Hint: Consider defining an inner generator function. What arguments should it take in? Where should you call it?

Use OK to test your code:

``python3 ok -q remainders_generator``

### Question 5: Project

Nothing to submit for this question, just a reminder to work on your project! The checkpoint is due April 22nd! Good luck!