We are no longer in the Google Python Course.
We may define the positive integers as follows:
The number 1 is a positive integer.
Any positive integer plus 1 is a positive integer.
Below we define a generator for integers. (We will discuss generators later.)
These examples are found in the python file recursion.py
def integers(n):
yield n
while True:
n +=1
yield n
intg = integers(1)
type(intg)
generator
next(intg)
1
next(intg)
2
next(intg)
3
newintg = integers(1000)
next(newintg)
1000
next(newintg)
1001
We may extend the definition to include all integers.
Any positive integer is an integer.
Any integer minus 1 is an integer.
Lists and strings are recursive data structures.
The empty list [] is a list.
Appending an item to a list results in a list.
Similarly for strings.
The empty string "" is a string.
Concatenating a character to a string results in a string.
We will see similar definitions for other data structures, including stacks, queues, trees, and graphs. FYI, the Facebook friends network is a graph.
Given a recursive data structure, it is convenient to use recursive functions to process the recursive structures. The two hallmarks of a recursive function are
A base case, such as the empty list or the empty string.
A traversal function for moving through the data structure.
Below we compare recursion to iteration and introduce tail recursion as a more efficient version.
def itotal(lst):
sum = 0
for n in lst:
sum += n
return sum
def ilength(lst):
length = 0
for i in lst:
length += 1
return length
def imax(lst):
m = lst[0]
for n in lst[1:]:
if n > m:
m = n
return m
l = list(range(1,6))
l
[1, 2, 3, 4, 5]
itotal(l)
15
ilength(l)
5
imax(l)
5
def rtotal(lst):
if not lst:
return 0
else:
return lst[0] + rtotal(lst[1:])
def rlength(lst):
if not lst:
return 0
else:
return 1 + rlength(lst[1:])
def rmax(lst):
if not lst:
return float('-inf')
else:
return max(lst[0], rmax(lst[1:]))
l = list(range(1,6))
l
[1, 2, 3, 4, 5]
sum(l)
15
rtotal(l)
15
len(l)
5
rlength(l)
5
max(l)
5
rmax(l)
5
Below we define trace() which allows us watch the execution of recursive functions. Later we will see that trace() can be used as a decorator.
def trace(f):
f.indent = 0
def g(x):
print('| ' * f.indent + '|--', f.__name__, x)
f.indent += 1
value = f(x)
print('| ' * f.indent + '|--', 'return', repr(value))
f.indent -= 1
return value
return g
rtotal = trace(rtotal)
rtotal(l)
|-- rtotal [1, 2, 3, 4, 5] | |-- rtotal [2, 3, 4, 5] | | |-- rtotal [3, 4, 5] | | | |-- rtotal [4, 5] | | | | |-- rtotal [5] | | | | | |-- rtotal [] | | | | | | |-- return 0 | | | | | |-- return 5 | | | | |-- return 9 | | | |-- return 12 | | |-- return 14 | |-- return 15
15
rlength = trace(rlength)
rlength(l)
|-- rlength [1, 2, 3, 4, 5] | |-- rlength [2, 3, 4, 5] | | |-- rlength [3, 4, 5] | | | |-- rlength [4, 5] | | | | |-- rlength [5] | | | | | |-- rlength [] | | | | | | |-- return 0 | | | | | |-- return 1 | | | | |-- return 2 | | | |-- return 3 | | |-- return 4 | |-- return 5
5
rmax = trace(rmax)
rmax(l)
|-- rmax [1, 2, 3, 4, 5] | |-- rmax [2, 3, 4, 5] | | |-- rmax [3, 4, 5] | | | |-- rmax [4, 5] | | | | |-- rmax [5] | | | | | |-- rmax [] | | | | | | |-- return -inf | | | | | |-- return 5 | | | | |-- return 5 | | | |-- return 5 | | |-- return 5 | |-- return 5
5
As should be apparent from the traces of the recursive functions above, there is a lot of overhead for calling recursive functions. The state of the calling function has to be pushed on the stack, waiting for the recursive calls to complete.
One way to avoid this overhead is to write tail recursive functions. That is, the function is recursive, but the calling function does not need to wait to complete the final value. The tail recursive call includes the needed result values.
def trtotal(lst):
return totalaux(lst,0)
def totalaux(lst, result):
if not lst:
return result
return totalaux(lst[1:], result + lst[0])
def trlength(lst):
return lengthaux(lst,0)
def lengthaux(lst, result):
if not lst:
return result
return lengthaux(lst[1:], result + 1)
def trmax(lst):
return maxaux(lst, float('-inf'))
def maxaux(lst, result):
if not lst:
return result
else:
if lst[0] > result:
result = lst[0]
return maxaux(lst[1:], result)
Here are revised versions of the tail recursive functions that do not require helper functions. They use default parameter values instead.
def trtotalbest(lst, result=0):
if not lst:
return result
return trtotalbest(lst[1:], result + lst[0])
def trlengthbest(lst, result=0):
if not lst:
return result
return trlengthbest(lst[1:], result + 1)
def trmaxbest(lst, result = float('-inf')):
if not lst:
return result
else:
if lst[0] > result:
result = lst[0]
return trmaxbest(lst[1:], result)
trtotalbest(l)
15
trlengthbest(l)
5
trmaxbest(l)
5
Let's try tracing these tail recursive functions.
trtotalbest = trace(trtotalbest)
trtotalbest(l)
|-- trtotalbest [1, 2, 3, 4, 5]
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-46-fdb1bacedf2e> in <module> ----> 1 trtotalbest(l) <ipython-input-29-1024a9c18831> in g(x) 4 print('| ' * f.indent + '|--', f.__name__, x) 5 f.indent += 1 ----> 6 value = f(x) 7 print('| ' * f.indent + '|--', 'return', repr(value)) 8 f.indent -= 1 <ipython-input-39-3c752ea99475> in trtotalbest(lst, result) 2 if not lst: 3 return result ----> 4 return trtotalbest(lst[1:], result + lst[0]) TypeError: g() takes 1 positional argument but 2 were given
Oops. We have a problem. The trace() function is assuming that the parameter function has a single parameter itself. The tr--best functions have a variable number of arguments due to the default parameter. We need to modify trace to handle the variable number of arguments.
def trace2(f):
f.indent = 0
def g(*x):
print('| ' * f.indent + '|--', f.__name__, x)
f.indent += 1
value = f(*x)
print('| ' * f.indent + '|--', 'return', repr(value))
f.indent -= 1
return value
return g
def trtotalbest(lst, result=0):
if not lst:
return result
return trtotalbest(lst[1:], result + lst[0])
trtotalbest = trace2(trtotalbest)
trtotalbest(l)
|-- trtotalbest ([1, 2, 3, 4, 5],) | |-- trtotalbest ([2, 3, 4, 5], 1) | | |-- trtotalbest ([3, 4, 5], 3) | | | |-- trtotalbest ([4, 5], 6) | | | | |-- trtotalbest ([5], 10) | | | | | |-- trtotalbest ([], 15) | | | | | | |-- return 15 | | | | | |-- return 15 | | | | |-- return 15 | | | |-- return 15 | | |-- return 15 | |-- return 15
15
The sad fact is that python does not optimize tail-recursive calls. See (https://chrispenner.ca/posts/python-tail-recursion). As the linked page shows, you can implement tail recursion, creating a Recurse class. We will revisit this topic later.
Here are a couple of additional recursive functions. These examples are related to the homework problems.
# toord('abcd') - iterative
def toord(str):
for c in str:
print (c, " => ", ord(c))
# recursive version
def rtoord(str):
if not str:
return
else:
c = str[0]
print (c, " => ", ord(c))
rtoord(str[1:])
toord('abcd')
a => 97 b => 98 c => 99 d => 100
rtoord('abcd')
a => 97 b => 98 c => 99 d => 100
def tochar(s, e):
for n in range(s, e):
print (n, " => ", chr(n))
def rtochar(s, e):
if s == e:
return
else:
print (s, " => ", chr(s))
rtochar(s+1, e)
tochar(97,101)
97 => a 98 => b 99 => c 100 => d
rtochar(97,101)
97 => a 98 => b 99 => c 100 => d
Above we discussed the use of recursion in providing succinct definitions of mathematical objects, like integers, and data structures, like lists.
Another important use of recursion is defining grammars for languages - both natural languages, like English and German, and computer languages, like Python and C.
You are no doubt familiar with parts of speech such as noun, verb, and prepositions. These are the non-terminal building blocks of a grammar. The terminal elements are words: nouns such as "cat" or "dog", and verbs such as "ran" or "chased".
Below is a simple grammar (in racket) for a trivial subset of English.
(define grammar-mcd (cfg '(a the mouse cat dog it slept swam chased evaded dreamed believed that) '(s np vp det n pn vi vt v3) 's (list (rule 's '(np vp)) (rule 'np '(det n)) (rule 'np '(pn)) (rule 'det '(a)) (rule 'det '(the)) (rule 'n '(mouse)) (rule 'n '(cat)) (rule 'n '(dog)) (rule 'pn '(it)) (rule 'vp '(vi)) (rule 'vp '(vt np)) (rule 'vp '(v3 that s)) (rule 'vi '(slept)) (rule 'vi '(swam)) (rule 'vt '(chased)) (rule 'vt '(evaded)) (rule 'v3 '(dreamed)) (rule 'v3 '(believed)))))
We interpret the grammar as follows:
Terminal values: a the mouse cat dog it slept swam chased evaded dreamed believed that
Non-terminal values: s np vp det n pn vi vt v3
Rewrite rules: (the left side can be rewritten as the right side)
s : np vp # a sentence is a noun phrase followed by a verb phrase np : det n # a noun phrase can be a determiner followed by a noun np : pn # a noun can be a pronoun det : a # a determiner can be a det : the # a determiner can be the n : mouse # a noun can be mouse n : cat # a noun can be cat n : dog # a noun can be dog pn : it # a pronoun can be it vp : vi # a verb phrase can be an intransitive verb vp : vt np # a verb phrase can be a transitive verb followed by a noun phrase vp : v3 that s # a verb phrase can be a v3 verb followed by "that" followed by a sentence vi : slept # an intransitive verb can be slept vi : swam # an intransitive verb can be swam vt : chased # a transitive verb can be chased vt : evaded # a transitive verb can be evaded v3: dreamed # a v3 verb can be dreamed v3 : believed # a v3 verb can be believed
We can derive a sentence by expanding each non-terminal node of a tree until no non-terminal nodes remain. This is a recursive process.
S -> NP VP NP -> DET N DET -> the N -> cat VP -> VT NP VT -> chased NP -> PN PN -> it the cat chased it
Note that the v3 rule results in a recursive call to the sentence node.
This type of grammar, with a single non-terminal on the left hand side, is known as a context free grammar. There is a more restrictive type of grammar known as regular expressions, which we will examine later.
There are also less restrictive grammars including context sensitive grammars and recursively enumerable languages. We will not be discussing those.
They all fall under the general category of formal languages.
Context free grammars are especially interesting for computer science as they provide a grammatical structure for most computer programming languages. The common name for these grammatical descriptions of programming languages is Backus Naur Form or BNF. John Backus designed FORTRAN and Peter Naur designed ALGOL.
See http://matt.might.net/articles/grammars-bnf-ebnf/ for a discussion of the language of languages.
End of Recursion notebook, for now. Later we will discuss deep recursion.