On Recursion

Agenda

  1. Recursion
    • stopping recursion: simplification & base cases
  2. Recursive "shapes":
    • Linear (single) recursion:
      • Factorial
      • Addition
      • Binary search
    • Tree (multiple) recursion: divide and conquer
      • Fibonacci numbers
      • Tower of Hanoi
      • Merge sort
      • Making change
  3. The Call Stack and Stack Frames
    • simulating recursion
    • debugging with pdb and %debug

1. Recursion

Recursive functions, directly or indirectly, call themselves.

Recursive solutions are applicable when a problem can be broken down into more easily solved sub-problems that resemble the original, and whose solutions can then be combined.

E.g., computing the combined price of a bunch of nested shopping bags of items:

In [89]:
class Bag:
    def __init__(self, price, *contents):
        self.price = price
        self.contents = contents
In [90]:
bag1 = Bag(10)
In [91]:
def price(bag):
    return bag.price
In [92]:
price(bag1)
Out[92]:
10
In [93]:
bag2 = Bag(5, Bag(3))
In [94]:
price(bag2) #issue: not compute Bag(3)
Out[94]:
5
In [95]:
def price(bag): #bag inside bag
    p = bag.price
    for b in bag.contents:
        p += b.price
    return p
In [102]:
bag2 = Bag(5, Bag(3))
price(bag2)
Out[102]:
8
In [97]:
def price(bag): #bag inside bag inside bag
    p = bag.price
    for b in bag.contents:
        p += b.price
        for c in b.contents:
            p += c.price
    return p
In [98]:
bag3 = Bag(5, Bag(4, Bag(3)), Bag(2))
price(bag3)
Out[98]:
14
In [ ]:
bag4 = Bag(0, Bag(5), Bag(10), Bag(3, Bag(2), Bag(100)), Bag(9, Bag(2, Bag(25))))
In [99]:
#recursive solution

def price(bag):
    p = bag.price
    for b in bag.contents:
        #code here
        p += price(b)
    return p
In [100]:
price(bag1)
Out[100]:
10
In [103]:
price(bag2)
Out[103]:
8
In [104]:
price(bag3)
Out[104]:
14
In [105]:
price(bag4)
Out[105]:
156

Stopping recursion: simplification & base case(s)

In [106]:
import sys
sys.setrecursionlimit(200) #define the max allowed depth of stack
In [131]:
def silly_rec(n):
    print(n)
    silly_rec(n)
In [132]:
silly_rec(1) #issues?
In [113]:
def silly_rec(n):
    print(n)
    if n == 0:
        # base case: no recursion
        print('Base case 1 hit')
    elif n == -100:
        # base case: no recursion
        print('Base case 2 hit')
    else:
        # recursive case: must make progress towards a base case
        silly_rec(n-1)
In [110]:
silly_rec(1)
1
0
Base case hit
In [114]:
silly_rec(10)
10
9
8
7
6
5
4
3
2
1
0
Base case 1 hit
In [115]:
silly_rec(-10)
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
-83
-84
-85
-86
-87
-88
-89
-90
-91
-92
-93
-94
-95
-96
-97
-98
-99
-100
Base case 2 hit

2. Recursive "shapes"

Linear recursion

Example: Factorial

$n! = \begin{cases} 1 & \text{if}\ n=0 \\ n \cdot (n-1)! & \text{if}\ n>0 \end{cases}$

i.e., $n! = n \cdot (n-1) \cdot (n-2) \cdots 3 \cdot 2 \cdot 1$

In [116]:
def rec_factorial(n):
    print('n = ', n)
    
    if n == 0: #base case
        return 1
    else:
        return n * rec_factorial(n-1)


rec_factorial(10)
n =  10
n =  9
n =  8
n =  7
n =  6
n =  5
n =  4
n =  3
n =  2
n =  1
n =  0
Out[116]:
3628800

Example: Addition of two positive numbers $m$, $n$

$m + n = \begin{cases} m & \text{if}\ n=0 \\ (m + 1) + (n - 1) & \text{if}\ n > 0 \end{cases}$

In [117]:
def add(m, n):
    print('m, n = ', (m, n))
    
    if n == 0:
        return m
    else:
        return add(m+1, n-1)
In [118]:
add(5, 0)
m, n =  (5, 0)
Out[118]:
5
In [119]:
add(5, 1)
m, n =  (5, 1)
m, n =  (6, 0)
Out[119]:
6
In [120]:
add(5, 5)
m, n =  (5, 5)
m, n =  (6, 4)
m, n =  (7, 3)
m, n =  (8, 2)
m, n =  (9, 1)
m, n =  (10, 0)
Out[120]:
10
In [121]:
def bin_search(x, lst):
    mid = len(lst)//2
    print(lst)
    
    if not lst: #base case 1, not found the item
        return False
    elif lst[mid] == x: #base case 2, found the item
        return True
    elif lst[mid] < x:
        return bin_search(x, lst[mid+1:])
    else: # lst[mid] > x
        return bin_search(x, lst[:mid])
In [122]:
bin_search(20, list(range(100)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
[20, 21, 22, 23, 24]
[20, 21]
[20]
Out[122]:
True
In [123]:
bin_search(-1, list(range(100)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
[0, 1, 2, 3, 4, 5]
[0, 1, 2]
[0]
[]
Out[123]:
False
In [124]:
bin_search(50.5, list(range(100)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74]
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62]
[51, 52, 53, 54, 55, 56]
[51, 52, 53]
[51]
[]
Out[124]:
False

Tree recursion

Example: Fibonacci numbers

$fib(n) = \begin{cases} 0 & \text{if}\ n=0 \\ 1 & \text{if}\ n=1 \\ fib(n-1) + fib(n-2) & \text{otherwise} \end{cases}$

i.e., 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

In [125]:
def rec_fib(n):
    #print('n = ', n)
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return rec_fib(n-1) + rec_fib(n-2)

rec_fib(5)
Out[125]:
5
In [126]:
rec_fib(6)
Out[126]:
8
In [127]:
[rec_fib(i) for i in range(20)]
Out[127]:
[0,
 1,
 1,
 2,
 3,
 5,
 8,
 13,
 21,
 34,
 55,
 89,
 144,
 233,
 377,
 610,
 987,
 1597,
 2584,
 4181]
In [128]:
rec_fib(35)
Out[128]:
9227465
In [129]:
rec_fib(36)
Out[129]:
14930352
In [203]:
n = 2**40
print(n)
1099511627776

Example: Tower of Hanoi

Setup: three rods, with one or more discs of different sizes all stacked on one rod, smallest (top) to largest (bottom). E.g.,

     ||          ||          ||     
     ==          ||          ||     
    ====         ||          ||     
   ======        ||          ||     
------------------------------------

Goal: move all the discs, one by one, to another rod, with the rules being that (1) only smaller discs can be stacked on larger ones and (2) only the top disc in a stack can be moved to another rod.

For three discs, as shown above, we would carry out the following sequence to move the stack to the rightmost rod. The rods are abbreviated L (left), M (middle), R (right):

  1. Move the small disc (0) from L to R
  2. Move the medium disc (1) from L to M
  3. Move 0 from R to M (R is empty)
  4. Move the large disc (2) from L to R
  5. Move 0 from M to L
  6. Move 1 from M to R
  7. Move 0 from L to R (done)

Can you come up with the sequence needed to move a stack of 4 discs from one rod to another? 5 discs? An arbitrary number of discs?

In [215]:
height = 5
towers = [[] for _ in range(3)]
towers[0] = list(range(height, 0, -1))

def move(frm, to):
    towers[to].append(towers[frm].pop(-1))
    display()

def hanoi(frm, to, using, levels): # move N discs (i.e. levels) from Rod "frm" to Rod "to" using the Rod "using"
    if levels == 1:
        move(frm, to)
    else:
        hanoi(frm, using, to, levels-1)
        move(frm, to)
        hanoi(using, to, frm, levels-1)
In [216]:
towers
Out[216]:
[[5, 4, 3, 2, 1], [], []]
In [217]:
from time import sleep
from IPython.display import clear_output

def display():
    clear_output(True)
    print('{:^12}'.format('||') * 3)
    for level in range(height, 0, -1):
        for t in towers:
            try:
                print('{:^12}'.format('==' * t[level-1]), end='')
            except IndexError:
                print('{:^12}'.format('||'), end='')
        print()
    print('-' * 36)
    sleep(1)
In [218]:
display()
     ||          ||          ||     
     ==          ||          ||     
    ====         ||          ||     
   ======        ||          ||     
  ========       ||          ||     
 ==========      ||          ||     
------------------------------------
In [209]:
#move(0,2)
In [219]:
#hanoi(0, 2, 1, 3)
In [220]:
hanoi(0, 2, 1, 5)
     ||          ||          ||     
     ||          ||          ==     
     ||          ||         ====    
     ||          ||        ======   
     ||          ||       ========  
     ||          ||      ========== 
------------------------------------

Example: Mergesort

In [221]:
def merge(l1, l2): # O(N), where N is the number of elements in the two lists
    merged = []
    i1 = i2 = 0
    while i1 < len(l1) or i2 < len(l2):
        if i2 == len(l2) or (i1 < len(l1) 
                             and l1[i1] < l2[i2]):
            merged.append(l1[i1])
            i1 += 1
        else:
            merged.append(l2[i2])
            i2 += 1
    return merged
In [222]:
l1 = [1, 5, 9]
l2 = [2, 6, 8, 11]
merge(l1, l2)
Out[222]:
[1, 2, 5, 6, 8, 9, 11]
In [223]:
def mergesort(lst):
    if len(lst) <= 1:
        return lst
    else:
        mid = len(lst)//2
        return merge(mergesort(lst[:mid]), mergesort(lst[mid:]))
In [224]:
import random
lst = list(range(10))
random.shuffle(lst)
In [225]:
lst
Out[225]:
[4, 3, 2, 9, 7, 1, 5, 8, 0, 6]
In [226]:
mergesort(lst)
Out[226]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [192]:
def mergesort(lst):
    if len(lst) <= 1:
        return lst
    else:
        mid = len(lst)//2
        l1 = mergesort(lst[:mid])
        l2 = mergesort(lst[mid:])
        #print('Merging', l1, 'and', l2)
        return merge(l1, l2)
In [194]:
mergesort(lst)
In [227]:
def insertion_sort(lst):
    for i in range(1, len(lst)):
        for j in range(i, 0, -1):
            if lst[j-1] > lst[j]:
                lst[j-1], lst[j] = lst[j], lst[j-1] # swap
            else:
                break
In [228]:
class Heap:
    def __init__(self):
        self.data = []

    @staticmethod
    def _parent(idx):
        return (idx-1)//2
        
    @staticmethod
    def _left(idx):
        return idx*2+1

    @staticmethod
    def _right(idx):
        return idx*2+2
    
    def _heapify(self, idx=0):
        while True:
            l = Heap._left(idx)
            r = Heap._right(idx)
            maxidx = idx
            if l < len(self) and self.data[l] > self.data[idx]:
                maxidx = l
            if r < len(self) and self.data[r] > self.data[maxidx]:
                maxidx = r
            if maxidx != idx:
                self.data[idx], self.data[maxidx] = self.data[maxidx], self.data[idx]
                idx = maxidx
            else:
                break
            
    def add(self, x):
        self.data.append(x)
        i = len(self.data) - 1
        p = Heap._parent(i)
        while i > 0 and self.data[p] < self.data[i]:
            self.data[p], self.data[i] = self.data[i], self.data[p]
            i = p
            p = Heap._parent(i)
        
    def max(self):
        return self.data[0]

    def pop_max(self):
        ret = self.data[0]
        self.data[0] = self.data[len(self.data)-1]
        del self.data[len(self.data)-1]
        self._heapify()
        return ret
    
    def __bool__(self):
        return len(self.data) > 0

    def __len__(self):
        return len(self.data)


def heapsort(iterable):
    heap = Heap()
    for x in iterable:
        heap.add(x)
    sorted_lst = []
    while heap:
        sorted_lst.append(heap.pop_max())
    sorted_lst.reverse()
    return sorted_lst
In [229]:
import timeit
import random
insertionsort_times = []
heapsort_times = []
mergesort_times = []
for size in range(100, 3000, 100):
    insertionsort_times.append(timeit.timeit(stmt='insertion_sort(lst)',
                               setup='import random ; from __main__ import insertion_sort ; '
                                         'lst = [random.random() for _ in range({})]'.format(size),
                               number=1))
    heapsort_times.append(timeit.timeit(stmt='heapsort(lst)',
                               setup='import random ; from __main__ import heapsort ; '
                                         'lst = [random.random() for _ in range({})]'.format(size),
                               number=1))
    mergesort_times.append(timeit.timeit(stmt='mergesort(lst)'.format(size),
                               setup='import random ; from __main__ import mergesort ; '
                                         'lst = [random.random() for _ in range({})]'.format(size),
                               number=1))
In [231]:
%matplotlib inline
import matplotlib.pyplot as plt
#plt.plot(insertionsort_times, 'ro')
plt.plot(heapsort_times, 'b^')
plt.plot(mergesort_times, 'gs')
plt.show()

Example: Making Change

Question: how many different ways are there of making up a specified amount of money, given a list of available denominations?

E.g., how many ways of making 10 cents, given 1c, 5c, 10c, 25c coins?

In [15]:
def change(amount, denoms):
    if amount == 0:
        return 1
    
    elif amount < 0 or not denoms:
        return 0
    
    else:
        return (change(amount-denoms[0], denoms) + change(amount, denoms[1:]))
In [16]:
change(5, (1, 5, 10, 25))
Out[16]:
2
In [17]:
change(10, (1, 5, 10, 25))
Out[17]:
4
In [18]:
change(10, (25, 10, 5, 1))
Out[18]:
4
In [19]:
change(100, (25, 10, 5, 1))
Out[19]:
242
In [20]:
# print out the solutions

def ways_change(amount, denoms, way=()):
    if amount == 0:
        print(way)
        return 1
    elif amount < 0 or not denoms:
        return 0
    else:
        return (ways_change(amount-denoms[0], denoms, way = way + (denoms[0],))
        + ways_change(amount, denoms[1:], way))
In [21]:
ways_change(10, (1, 5))
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
(1, 1, 1, 1, 1, 5)
(5, 5)
Out[21]:
3
In [22]:
ways_change(25, (1, 5, 10, 25))
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 10)
(1, 1, 1, 1, 1, 5, 5, 5, 5)
(1, 1, 1, 1, 1, 5, 5, 10)
(1, 1, 1, 1, 1, 10, 10)
(5, 5, 5, 5, 5)
(5, 5, 5, 10)
(5, 10, 10)
(25,)
Out[22]:
13

Key observation of the "Make Change" example

the number of ways we can make up some amount $N$ using $k$ different denominations is equal to:

the number of ways we can make $N$ excluding some denomination $d_i$ + the number of ways we can make $(N−d_i)$ using all $k$ denominations

[Note] Dynamic Programming Solution, instead of recursive call on same state, save results of state in a table and look them up.

3. The Call Stack

Simulating recursive factorial

In [23]:
class Stack(list):
    push = list.append
    pop  = lambda self: list.pop(self, -1)
    peek = lambda self: self[-1]
    empty = lambda self: len(self) == 0
In [24]:
#simulate a call stack

call_stack = Stack()

def call(arg):
    call_stack.push('<frame begin>')
    call_stack.push(('arg', arg))

def get_arg():
    return call_stack.peek()[-1]

def save_local(name, val): #save a local variable, avoid being overwritten
    call_stack.push(('local', name, val))
    
def restore_local(): #return a local variable
    return call_stack.pop()[2]
    
def return_with(val):
    while call_stack.pop() != '<frame begin>':
        pass
    call_stack.push(('ret', val))
    
def last_return_val():
    return call_stack.pop()[-1]
In [25]:
# simulate all the calls that goes down to the base case

call(10) # initial call (with argument 10)
while True: # recursive calls
    n = get_arg()
    if n == 1:
        return_with(1)
        break
    else:
        save_local('n', n)
        call(n-1)
In [26]:
call_stack
Out[26]:
['<frame begin>',
 ('arg', 10),
 ('local', 'n', 10),
 '<frame begin>',
 ('arg', 9),
 ('local', 'n', 9),
 '<frame begin>',
 ('arg', 8),
 ('local', 'n', 8),
 '<frame begin>',
 ('arg', 7),
 ('local', 'n', 7),
 '<frame begin>',
 ('arg', 6),
 ('local', 'n', 6),
 '<frame begin>',
 ('arg', 5),
 ('local', 'n', 5),
 '<frame begin>',
 ('arg', 4),
 ('local', 'n', 4),
 '<frame begin>',
 ('arg', 3),
 ('local', 'n', 3),
 '<frame begin>',
 ('arg', 2),
 ('local', 'n', 2),
 ('ret', 1)]
In [35]:
ret = last_return_val()
n = restore_local()
return_with(n * ret)
call_stack
Out[35]:
[('ret', 3628800)]
In [36]:
def rec_factorial(n):    
    if n == 0: #base case
        return 1
    else:
        return n * rec_factorial(n-1)


rec_factorial(10)
Out[36]:
3628800

Debugging with pdb and %debug

In [37]:
import sys
sys.setrecursionlimit(100)
In [38]:
def rec_factorial(n):
    if n <= 1:   # detect base case
        raise Exception('base case!')
    else:
        return n * rec_factorial(n-1)
In [39]:
rec_factorial(10)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-39-982f4daae583> in <module>()
----> 1 rec_factorial(10)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-38-7a88057e3c85> in rec_factorial(n)
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
----> 3         raise Exception('base case!')
      4     else:
      5         return n * rec_factorial(n-1)

Exception: base case!
In [40]:
%debug
# commands to try:
# help, where, args, p n, up, u 10, down, d 10, l, up 100, u, d (& enter to repeat)

# The most convenient interactive interface to debugging is the %debug magic command. 
# If you call it after hitting an exception, 
# it will automatically open an interactive debugging prompt at the point of the exception. 
# The ipdb prompt lets you explore the current state of the stack, 
# explore the available variables, and even run Python commands!
> <ipython-input-38-7a88057e3c85>(3)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
----> 3         raise Exception('base case!')
      4     else:
      5         return n * rec_factorial(n-1)

ipdb> help

Documented commands (type help <topic>):
========================================
EOF    cl         disable  interact  next    psource  rv         unt   
a      clear      display  j         p       q        s          until 
alias  commands   down     jump      pdef    quit     source     up    
args   condition  enable   l         pdoc    r        step       w     
b      cont       exit     list      pfile   restart  tbreak     whatis
break  continue   h        ll        pinfo   return   u          where 
bt     d          help     longlist  pinfo2  retval   unalias  
c      debug      ignore   n         pp      run      undisplay

Miscellaneous help topics:
==========================
exec  pdb

ipdb> where
  <ipython-input-39-982f4daae583>(1)<module>()
----> 1 rec_factorial(10)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

  <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

> <ipython-input-38-7a88057e3c85>(3)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
----> 3         raise Exception('base case!')
      4     else:
      5         return n * rec_factorial(n-1)

ipdb> print n
*** SyntaxError: Missing parentheses in call to 'print'. Did you mean print(n)?
ipdb> p n
1
ipdb> up
> <ipython-input-38-7a88057e3c85>(5)rec_factorial()
      1 def rec_factorial(n):
      2     if n <= 1:   # detect base case
      3         raise Exception('base case!')
      4     else:
----> 5         return n * rec_factorial(n-1)

ipdb> p n
2
ipdb> q

pdb - The typical usage to break into the debugger from a running program is to insert import pdb; pdb.set_trace() at the location you want to break into the debugger.

You can then step through the code following this statement, and continue running without the debugger using the continue command.

  • list - show code

  • continue - run to next break

  • next - execute current line, NOT stopping within functions in that line

  • step - execute current line, stopping within functions in that line

  • p expression - print the expression

  • args - Print the argument list of the current function.

  • quit

In [ ]:
def bin_search(x, lst):
    if len(lst) == 0:
        return False
    else:
        # print('lo, hi = ', (lst[0], lst[-1]))
        mid = len(lst) // 2
        if x == lst[mid]:
            import pdb ; pdb.set_trace()
            return True
        elif x < lst[mid]:
            return bin_search(x, lst[:mid])
        else:
            return bin_search(x, lst[mid+1:])
In [ ]:
bin_search(20, list(range(100)))
> <ipython-input-41-5c53355c76d6>(9)bin_search()
-> return True
(Pdb) where
  /anaconda3/lib/python3.6/runpy.py(193)_run_module_as_main()
-> "__main__", mod_spec)
  /anaconda3/lib/python3.6/runpy.py(85)_run_code()
-> exec(code, run_globals)
  /anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py(16)<module>()
-> app.launch_new_instance()
  /anaconda3/lib/python3.6/site-packages/traitlets/config/application.py(658)launch_instance()
-> app.start()
  /anaconda3/lib/python3.6/site-packages/ipykernel/kernelapp.py(486)start()
-> self.io_loop.start()
  /anaconda3/lib/python3.6/site-packages/tornado/platform/asyncio.py(127)start()
-> self.asyncio_loop.run_forever()
  /anaconda3/lib/python3.6/asyncio/base_events.py(422)run_forever()
-> self._run_once()
  /anaconda3/lib/python3.6/asyncio/base_events.py(1432)_run_once()
-> handle._run()
  /anaconda3/lib/python3.6/asyncio/events.py(145)_run()
-> self._callback(*self._args)
  /anaconda3/lib/python3.6/site-packages/tornado/ioloop.py(759)_run_callback()
-> ret = callback()
  /anaconda3/lib/python3.6/site-packages/tornado/stack_context.py(276)null_wrapper()
-> return fn(*args, **kwargs)
  /anaconda3/lib/python3.6/site-packages/zmq/eventloop/zmqstream.py(536)<lambda>()
-> self.io_loop.add_callback(lambda : self._handle_events(self.socket, 0))
  /anaconda3/lib/python3.6/site-packages/zmq/eventloop/zmqstream.py(450)_handle_events()
-> self._handle_recv()
  /anaconda3/lib/python3.6/site-packages/zmq/eventloop/zmqstream.py(480)_handle_recv()
-> self._run_callback(callback, msg)
  /anaconda3/lib/python3.6/site-packages/zmq/eventloop/zmqstream.py(432)_run_callback()
-> callback(*args, **kwargs)
  /anaconda3/lib/python3.6/site-packages/tornado/stack_context.py(276)null_wrapper()
-> return fn(*args, **kwargs)
  /anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py(283)dispatcher()
-> return self.dispatch_shell(stream, msg)
  /anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py(233)dispatch_shell()
-> handler(stream, idents, msg)
  /anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py(399)execute_request()
-> user_expressions, allow_stdin)
  /anaconda3/lib/python3.6/site-packages/ipykernel/ipkernel.py(208)do_execute()
-> res = shell.run_cell(code, store_history=store_history, silent=silent)
  /anaconda3/lib/python3.6/site-packages/ipykernel/zmqshell.py(537)run_cell()
-> return super(ZMQInteractiveShell, self).run_cell(*args, **kwargs)
  /anaconda3/lib/python3.6/site-packages/IPython/core/interactiveshell.py(2662)run_cell()
-> raw_cell, store_history, silent, shell_futures)
  /anaconda3/lib/python3.6/site-packages/IPython/core/interactiveshell.py(2785)_run_cell()
-> interactivity=interactivity, compiler=compiler, result=result)
  /anaconda3/lib/python3.6/site-packages/IPython/core/interactiveshell.py(2909)run_ast_nodes()
-> if self.run_code(code, result):
  /anaconda3/lib/python3.6/site-packages/IPython/core/interactiveshell.py(2963)run_code()
-> exec(code_obj, self.user_global_ns, self.user_ns)
  <ipython-input-42-b5349e06e942>(1)<module>()
-> bin_search(20, list(range(100)))
  <ipython-input-41-5c53355c76d6>(11)bin_search()
-> return bin_search(x, lst[:mid])
  <ipython-input-41-5c53355c76d6>(11)bin_search()
-> return bin_search(x, lst[:mid])
  <ipython-input-41-5c53355c76d6>(13)bin_search()
-> return bin_search(x, lst[mid+1:])
  <ipython-input-41-5c53355c76d6>(13)bin_search()
-> return bin_search(x, lst[mid+1:])
  <ipython-input-41-5c53355c76d6>(11)bin_search()
-> return bin_search(x, lst[:mid])
  <ipython-input-41-5c53355c76d6>(11)bin_search()
-> return bin_search(x, lst[:mid])
> <ipython-input-41-5c53355c76d6>(9)bin_search()
-> return True
(Pdb) p lst
[20]
(Pdb) up
> <ipython-input-41-5c53355c76d6>(11)bin_search()
-> return bin_search(x, lst[:mid])
(Pdb) p lst
[20, 21]
(Pdb) down
> <ipython-input-41-5c53355c76d6>(9)bin_search()
-> return True
(Pdb) p lst
[20]
In [ ]: