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).
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.
The generators have the potential to generate an infinite sequence of values. For example:
def count(start = 0, step = 1): # optional parameters
i = start
while True: # read: forever!
yield i
print("Now incrementing i=", i)
i += step
g = count()
next(g)
next(g)
next(g)
next(g)
next(g)
newG = count(10, 3)
next(newG)
next(newG)
step # do local variables inside the function have any meaning outside?
i # do local variables inside the function have any meaning outside?
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$.
def fibo(a = 0, b = 1):
yield a
yield b
while True:
a,b = b,a+b
yield b
fibN = fibo()
next(fibN)
next(fibN)
next(fibN)
next(fibN)
next(fibN)
next(fibN)
for i, num in zip(range(5), fibN):
print(num)
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.
def readNameGen(filename):
for line in open(filename):
yield line.strip()
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.
next(nameGen)
next(nameGen)
next(nameGen)
next(nameGen)
for num, city in zip(range(10), nameGen): # give me next 10
print(city)
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).
def palindromeNames(filename):
for line in open(filename):
name = line.strip().lower() # what should I write here
if name == name[::-1]:
yield name
vNameGen = palindromeNames('wordlists/firstNames')
next(vNameGen)
next(vNameGen)
next(vNameGen)
for sname in palindromeNames('wordlists/firstNames'):
print(sname) # predict the output
Iterating over the generator object in a for loops will yield all values from the beginning until StopIteration
exception is raised.
for i in range(5): # predict the output here
print(next(vNameGen))
next(vNameGen)
next(vNameGen)
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).
def reverseGen(seq):
for item in seq[::-1]:
yield item
cityList = [city.strip() for city in open('wordlists/cities')]
rev = reverseGen(cityList)
next(rev)
next(rev)
next(rev)
next(rev)
next(rev)
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?
for i in range(10):
print(next(rev))
for i in range(10): # give me next 10 in reverse from where we left off
print(next(rev))
for i in range(10): # give me next 10 in reverse from where we left off
print(next(rev))
randomFortune
¶Let us write a generator that given a list of fortunes yeilds a random fortune from the list.
import random
random.randint(0, 10) # check what it does
random.randint(0, 10) # check what it does
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]
fortuneGen = randomFortunes()
next(fortuneGen)
next(fortuneGen)
for i, f in zip(range(5), fortuneGen):
print(f)
Summary. Generator functions are "resumable" functions that let us generate a sequence of (possibly infinite) values on on the fly!
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
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!
myList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
listIter = iter(myList)
listIter
next(listIter)
next(listIter)
for num in listIter: # remembers state
print(num)