Lecture 13: Generators and Iterators

In today's lecture we will finish up with our leftovers (plotting examples using matplotlib) and move on to a new topic: generator functions in Python.

Acknowlegement. This notebook has been adapted from the Wellesley CS111 Spring 2019 course materials (http://cs111.wellesley.edu/spring19).

Review: Generators

A generator is an object that constructs a (possibly infinite) stream of values on demand.

Whenever we write a function that mentions the yield keyword, the result of the function, when called, is a generator.

Generating Infinites Sequences

The generators have the potential to generate an infinite sequence of values. For example:

In [1]:
def count(start = 0, step = 1): # optional parameters
    i = start
    while True: # read: forever!
        yield i
        print("Now incrementing i=", i)
        i += step
In [2]:
g = count()
In [3]:
next(g)
Out[3]:
0
In [4]:
next(g)
Now incrementing i= 0
Out[4]:
1
In [5]:
next(g)
Now incrementing i= 1
Out[5]:
2
In [6]:
next(g)
Now incrementing i= 2
Out[6]:
3
In [7]:
next(g)
Now incrementing i= 3
Out[7]:
4
In [8]:
newG = count(10, 3)
In [9]:
next(newG)
Out[9]:
10
In [10]:
next(newG)
Now incrementing i= 10
Out[10]:
13
In [11]:
step  # do local variables inside the function have any meaning outside?
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-11-a4a9c91b505b> in <module>
----> 1 step  # do local variables inside the function have any meaning outside?

NameError: name 'step' is not defined
In [12]:
i  # do local variables inside the function have any meaning outside?
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-6e0273dbb376> in <module>
----> 1 i  # do local variables inside the function have any meaning outside?

NameError: name 'i' is not defined

Fibonacci Sequence

Let us write a generator funcion that generates an infinite series of Fibonacci numbers on demand. In mathematics, the Fibonacci numbers, commonly denoted $F_n$, form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,

$F_0 = 0$, $F_1 = 1$, and $F_n = F_{n-2} + F_{n-2}$ for all $n \geq 2$.

In [13]:
def fibo(a = 0, b = 1):
    yield a
    yield b
    while True:
        a,b = b,a+b
        yield b
In [14]:
fibN = fibo()
In [15]:
next(fibN)
Out[15]:
0
In [16]:
next(fibN)
Out[16]:
1
In [17]:
next(fibN)
Out[17]:
1
In [18]:
next(fibN)
Out[18]:
2
In [19]:
next(fibN)
Out[19]:
3
In [20]:
next(fibN)
Out[20]:
5
In [21]:
for i, num in zip(range(5), fibN):
    print(num)
8
13
21
34
55

Fun with Generators!

Let us write generator funcions that take an iterable sequence (e.g. list, file, tuples, range, etc.) and yields values from the sequence in some order.

readNameGen

Lets write a generator function readNameGen that takes the path to a file as input and yields one line of the file at a time. We can call it on some of the files from wordlists such as cities and firstNames and play with it.

In [22]:
def readNameGen(filename):
    for line in open(filename):
        yield line.strip()
In [23]:
nameGen = readNameGen('wordlists/cities')

nameGen is now a generator object, which, when its next method is called will yield names of cities from the cities file one by a time.

In [24]:
next(nameGen)
Out[24]:
'Abilene'
In [25]:
next(nameGen)
Out[25]:
'Akron'
In [26]:
next(nameGen)
Out[26]:
'Alameda'
In [27]:
next(nameGen)
Out[27]:
'Albany'
In [28]:
for num, city in zip(range(10), nameGen):  # give me next 10
    print(city)
Albany
Albany
Albuquerque
Alexandria
Alhambra
Aliso Viejo
Allen
Allentown
Alpharetta
Amarillo

palindromeNames

Lets write a generator function palindromeNames that takes the path to a file containing first names as input and yields (one at a time) all those names from the file (in order) that are palindromes (read the same forward and backwards).

In [29]:
def palindromeNames(filename):
    for line in open(filename):
        name = line.strip().lower() # what should I write here
        if name == name[::-1]:
            yield name
In [30]:
vNameGen = palindromeNames('wordlists/firstNames')
In [31]:
next(vNameGen)
Out[31]:
'ada'
In [32]:
next(vNameGen)
Out[32]:
'aja'
In [33]:
next(vNameGen)
Out[33]:
'alla'
In [34]:
for sname in palindromeNames('wordlists/firstNames'):
    print(sname)  # predict the output
ada
aja
alla
ana
anna
ara
asa
ava
bob
cuc
eve
hannah
nan
otto

Iterating over the generator object in a for loops will yield all values from the beginning until StopIteration exception is raised.

In [35]:
for i in range(5): # predict the output here
    print(next(vNameGen))
ana
anna
ara
asa
ava
In [36]:
next(vNameGen)
Out[36]:
'bob'
In [37]:
next(vNameGen)
Out[37]:
'cuc'

reverseGen

Lets write a generator function reverseGen that takes a sequence (again, a list or string or tuple etc.) and yields values from the sequence from the end of the sequence (one at a time).

In [38]:
def reverseGen(seq):
    for item in seq[::-1]:
        yield item
In [39]:
cityList = [city.strip() for city in open('wordlists/cities')]
In [40]:
rev = reverseGen(cityList)
In [41]:
next(rev)
Out[41]:
'Yuma'
In [42]:
next(rev)
Out[42]:
'Yucaipa'
In [43]:
next(rev)
Out[43]:
'Yuba City'
In [44]:
next(rev)
Out[44]:
'Youngstown'
In [45]:
next(rev)
Out[45]:
'Yorba Linda'

Question. Will the for loop below start iterating over the list cityList from the end again? Will we see the same words we have seen or different ones?

In [46]:
for i in range(10):  
    print(next(rev))
Yonkers
Yakima
Wyoming
Worcester
Woodland
Woodbury
Winston
Wilmington
Wilmington
Wichita Falls
In [47]:
for i in range(10):  # give me next 10 in reverse from where we left off
    print(next(rev))
Wichita
Whittier
White Plains
Wheaton
Weymouth Town
Weston
Westminster
Westminster
Westland
West Valley City
In [48]:
for i in range(10):  # give me next 10 in reverse from where we left off
    print(next(rev))
West Sacramento
West Palm Beach
West New York
West Jordan
West Haven
West Des Moines
West Covina
West Allis
Wellington
Waukesha

randomFortune

Let us write a generator that given a list of fortunes yeilds a random fortune from the list.

In [49]:
import random
In [50]:
random.randint(0, 10) # check what it does
Out[50]:
3
In [51]:
random.randint(0, 10)  # check what it does
Out[51]:
10
In [52]:
def randomFortunes():
    """reads in a filename 'fortunes.txt' and generates
    random lines (a fortune) from it one at a time"""
    fortunes = [fortune.strip() for fortune in open('fortunes.txt')]
    while True:
        index = random.randint(0, len(fortunes)-1)
        yield fortunes[index]
In [53]:
fortuneGen = randomFortunes()
In [54]:
next(fortuneGen)
Out[54]:
'you will gain the ability to stop time by snapping your fingers'
In [55]:
next(fortuneGen)
Out[55]:
'you will gain the ability to stop time by snapping your fingers'
In [56]:
for i, f in zip(range(5), fortuneGen):
    print(f)
your family will be prosperous and healthy
driscoll will have salmon for dinner tonight.
every movie you see will be as good as Parasite
every movie you see will be as good as Parasite
you will live the life you are meant to

Summary. Generator functions are "resumable" functions that let us generate a sequence of (possibly infinite) values on on the fly!

New Topic: Iterators (via Generators)

All sequences in Python are iterable, they can be iterated over. Examples, strings, lists, ranges, tuples, files.

A Python object is iterable if it supports the iter function—that is, it has the special method iter defined—and returns an iterator.

An iterator is something that supports

  • the next function (that is, the special method next is defined and can be called to access the next item)
  • throws a StopIteration when the iterator “has run dry”
  • returns itself under an iter call

Iterations may be defined using class (with special methods iter and next implemented). However, as we will see Generators provide an easy way to define iterators in Python!

In [57]:
myList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
In [58]:
listIter = iter(myList)
In [59]:
listIter
Out[59]:
<list_iterator at 0x1105a3588>
In [60]:
next(listIter)
Out[60]:
1
In [61]:
next(listIter)
Out[61]:
2
In [62]:
for num in listIter:  # remembers state
    print(num)
3
4
5
6
7
8
9
10