1. What method(s) must iterables have?
  2. __iter__
  3. What method(s) must iterators have?
  4. __iter__ AND __next__
  5. What data type does __iter__ return?
  6. iterator
  7. What data type does __next__ return?
  8. whatever type each element of the sequence is
  9. Is a generator an iterable or an iterator?
  10. iterator
  11. What keyword in the body of a function makes that function return a generator object?
  12. yield
  13. How is yield different from return?
  14. yield does not close the frame. yield outputs a value, and keeps the frame open until StopIteration is raised.
  15. When you call next on a generator, the body starts executing at what line? At what line will it stop? At what line will it start the next time you call next?
  16. execution starts at the first line of the body and stops after the line with yield is executed. when next is called again, execution picks up at the line right after the yield statement (where it last left off) and stops after another yield execution or if there are no more lines in the body.
  17. What happens when you call list on an iterable or an iterator? What happens if you call it a second time on the same objects?
  18. when you call list on an iterable or an iterator, Python calls iter on it and then attempts to construct a list with the next element until StopIteration is reached, then returns the constructed list.
  19. Can you iterate through an iterable in a for loop? Can you iterate through an iterator in a for loop?
  20. you can use both iterables and iterators in a for loop.


What would Python print?

The following classes define an iterable representing the sequence of multiples for any given number and the iterator that returns the next value in the sequence. The sequence goes up to 1000.

class Multiples:
    def __init__(self, num):
        self.num = num
    def __iter__(self):
        return MultiplesIterator(self.num)

class MultiplesIterator:
    def __init__(self, num):
        self.num = num
        self.curr = num
    def __iter__(self):
        return self
    def __next__(self):
        if self.curr >= 1000:
            raise StopIteration
        val = self.curr
        self.curr = self.curr + self.num
        return val

What will the following lines output?

>>> hundreds = Multiples(100)
>>> next(hundreds) TypeError: 'Multiples' object is not an iterator >>> next(iter(hundreds)) 100 >>> next(iter(hundreds)) 100 >>> i = iter(hundreds) >>> i is iter(hundreds) False >>> i is iter(i) True >>> next(i) 100 >>> next(i) 200 >>> list(hundreds) [100, 200, 300, 400, 500, 600, 700, 800, 900] >>> list(i) [300, 400, 500, 600, 700, 800, 900] >>> list(hundreds) [100, 200, 300, 400, 500, 600, 700, 800, 900] >>> list(i) [] >>> for i in hundreds: ... print(i) 5
>>> for x in i: ... print(x)

Explanation: Multiples's __iter__ method always returns a *new instance* of MultiplesIterator, whereas MultiplesIterator's __iter__ simply returns itself. For iterables, calling iter will reset the iterator, but since iterators just return themselves, they do not reset.


Writing code

Fill in the following definition of a generator function which yields every number from 1 to n and prints 'm was a factor' if the previous number, m, was a factor of n. See the doctests for an example.

def print_factor(n):
    >>> gen = print_factor(8)
    >>> next(gen)
    >>> next(gen)
    1 was a factor
    >>> next(gen)
    2 was a factor
    >>> next(gen)
    >>> next(gen)
    4 was a factor
    "*** YOUR CODE HERE ***"


def print_factor(n):
    curr = 1
    while curr <= n:
        yield curr
        if n % curr == 0:
            print(str(curr) + " was a factor")
        curr += 1