CSE 102 – Tutorial 12 – Exceptions, functional programming
Sections tagged with a green marginal line are mandatory.
Sections tagged with an orange marginal line are either more advanced or supplementary, and are therefore considered optional. They will help you deepen your understanding of the material in the course. You may skip these sections on a first pass through the tutorial, and come back to them after finishing the mandatory exercises.
After you are finished with the tutorial, please upload your submission to Moodle by the indicated deadline.
Discussion of the tutorials with friends and classmates is allowed, with acknowledgment, but please read the Moodle page on “Collaboration, plagiarism, and AI”.
We expect you to write your solutions to all of the problems for this
tutorial in a file named exfun.py
and to upload it using
the form below. You can resubmit your solution as many times as you
like.
Programming with exceptions
Python comes with a built-in function input()
that reads
a single line from standard input. We can learn a bit
more about its behavior by typing help(input)
into the
Python interpreter (you’ll get slightly different output in Spyder):
>>> help(input)
Help on built-in function input in module builtins:
input(prompt='', /)
Read a string from standard input. The trailing newline is stripped.
The prompt string, if given, is printed to standard output without a
trailing newline before reading input.
If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.
On *nix systems, readline is used if available.
The EOF (end-of-file) character is traditionally used by terminal
programs in Unix systems to indicate that there is no more input. To
observe the behavior of input()
described above, execute
the program
while True: print('read: ' + input())
and try entering a few lines of text, followed by an EOF character. (Press “Control-D” to generate an EOF if you are running Linux/MacOS, or “Control-Z+Return” if you are running Windows.)
Write a function
sum_of_input()
which lets the user enter a list of integers and calculates their sum while performing validation of the input. The user should enter one number per line and indicate that they have finished typing numbers by sending an EOF. If the user enters an invalid integer, then your function must raise an exception namedInvalidInput
; on success, it should return the computed sum. You can write your function by completing the code below:class InvalidInput(Exception): pass def sum_of_input() -> int: # do something here
Hint: you can test that the input string is a valid integer by applying the
int
builtin. For example,int('-3')
is accepted and returns-3
, whileint('green')
raises aValueError
exception.
Previously in class, we saw how to define a function
timed(f, x)
which takes a function f
and an
argument x
and returns a pair (v, t)
consisting of the value computed by the function on the argument
together with the time needed for the function call. Let’s recall the
definition of this (higher-order) function, now with added type hints.
You should first include the lines
import time
from typing import Callable
at the top of your file, then you can consider the following definition:
def timed[S,T](f: Callable[[S],T], x: S) -> tuple[T, float]:
= time.time()
start = f(x)
out = time.time()
end return (out, end-start)
As indicated by the type annotations, the function
timed(f, x)
is generic in the sense that the
function f
can take an input argument of an arbitrary type
S
(and return an arbitrary type T
), so long as
x
has a matching type.
The bracket syntax for typing generic functions by specifying a list
of type
parameters was only introduced in Python 3.12. If you are using
Python 3.11 or earlier you should remove the suffix [S,T]
following def timed
and add the following two lines
beforehand
S = TypeVar('S')
T = TypeVar('T')
after making sure to import TypeVar
from the
typing
module.
For example, f
can be a function from integers to
integers, or a function that takes a string and returns nothing:
1]: timed(lambda x:x+1, 10)
In [1]: (11, 2.6226043701171875e-06)
Out[
2]: timed(print, 'hello')
In [
hello2]: (None, 5.650520324707031e-05) Out[
However, if the function f
raises an exception on input
x
, then timed(f, x)
will pass that exception
through to the caller and abort the timing operation:
3]: timed(lambda x:100000**100000/x, 0)
In [*** ZeroDivisionError: division by zero
Modify the behavior above so that if the function
f
raises an exceptione
on inputx
, then the timing function returns a pair(e, t)
of the exception itself together with the elapsed time until the exception was raised. You should implement this behavior as a new functiontimed_ex(f, x)
, which we declare below with type annotations.def timed_ex[S,T](f: Callable[[S],T], x: S) -> tuple[T | Exception, float]: # do something here
Examples:
4]: timed_ex(lambda x:x+1, 10) In [4]: (11, 4.0531158447265625e-06) Out[ 5]: timed_ex(lambda x:100000**100000/x, 0) In [5]: (ZeroDivisionError('division by zero'), 0.08045339584350586) Out[ 6]: timed_ex(int, 'green') In [6]: (ValueError("invalid literal for int() with base 10: 'green'"), 1.5497207641601562e-05) Out[
Hint: use a
try ... except ... finally
block.
Programming with binary trees, revisited
Throughout this section we will work with leaf-labelled binary trees, as discussed in class. We recall the encoding below — for simplicity, we assume that all leaves are labelled by integers.
class Leaf:
def __init__(self, value: int):
self.value = value
def __repr__(self):
return f'Leaf({self.value})'
class Node:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right
def __repr__(self):
return f'Node({self.left}, {self.right})'
type Tree = Leaf | Node
# remove "type" from the previous line for Python 3.11, i.e., replace by
# Tree = Leaf | Node
You should copy these definitions into a file named
trees.py
. (Or alternatively, download the file here.) Then you should add the line
from trees import Leaf, Node, Tree
to your main source file.
For example, the values
= Node(Leaf(0), Node(Leaf(1), Leaf(2)))
example1 : Tree = Node(Node(Leaf(0), Leaf(1)), Leaf(2))
example2 : Tree = Node(Node(Node(Leaf(0), Leaf(1)), Leaf(2)), Node(Leaf(3), Leaf(4))) example3 : Tree

Also useful for testing purposes, the function
cbt(n, x=0)
defined below constructs a complete binary tree
with \(2^n\) leaves labelled from \(x\) to \(x+2^n-1\).
def cbt(n: int, x:int = 0) -> Tree:
if n == 0: return Leaf(x)
else: return Node(cbt(n-1, x), cbt(n-1, x + 2**(n-1)))
Pattern-matching and folds
Making use of recursion and structural pattern-matching (i.e., Python’s
match ... case ...
syntax), write four functionscount_nodes(t)
,sum_leaves(t)
,leaves(t)
, andmirror(t)
with the following specifications:count_nodes(t: Tree) -> int
returns the number of binary nodes in the treet
;sum_leaves(t: Tree) -> int
returns the sum of the labels of the leaves in the treet
;leaves(t: Tree) -> list[int]
returns the list of leaf labels in the treet
, listed from left to right;mirror(t: Tree) -> Tree
returns the mirror image of the treet
.
Examples:
7]: [(count_nodes(t), sum_leaves(t), leaves(t), mirror(t)) for t in [example1,example2,example3]] In [7]: [(2, 3, [0, 1, 2], Node(Node(Leaf(2), Leaf(1)), Leaf(0))), Out[2, 3, [0, 1, 2], Node(Leaf(2), Node(Leaf(1), Leaf(0)))), (4, (10, 0, 1, 2, 3, 4], [4), Leaf(3)), Node(Node(Leaf(2), Leaf(1)), Leaf(0))))] Node(Node(Leaf( 8]: [count_nodes(cbt(n)) for n in range(10)] In [8]: [0, 1, 3, 7, 15, 31, 63, 127, 255, 511] Out[
Warning: Make sure that you have imported the definitions of the tree classes from the
trees
module as per the above instructions, rather than copying them into yourexfun.py
file. If you do the latter then your code will not pass the testing server!
You probably implemented all four functions from the previous exercise in a very similar way. In fact, they can all be written as folds over a binary tree in the following sense.
Write a higher-order function
fold_tree(t, f_leaf, f_node)
which takes as input a binary treet
, a unary functionf_leaf
, and a binary functionf_node
, and computes a value as follows: the functionf_leaf
is applied to obtain a value for each leaf label, and these values are combined by usingf_node
at every binary node. For example,fold_tree(example1, g, h)
should evaluate toh(g(0), h(g(1), g(2)))
for any functionsg
andh
.Examples:
9]: fold_tree(cbt(4), lambda x:x, min) In [10]: 0 Out[ 11]: fold_tree(cbt(4), lambda x:x, max) In [12]: 15 Out[
- If you have not already done so, add type annotations to your
definition of
fold_tree
, trying to write a type for the function that is as general as possible. (You might look to the examples oftimed
andtimed_ex
above.)
Reimplement the four functions that you defined above, but now just in terms of
fold_tree
. You should do so by defining the functionsf_leaf*
andf_node*
referenced below or by replacing them with appropriate lambda expressions.def count_nodes_as_fold(t : Tree) -> int: return fold_tree(t, f_leaf1, f_node1) def sum_leaves_as_fold(t : Tree) -> int: return fold_tree(t, f_leaf2, f_node2) def leaves_as_fold(t : Tree) -> list[int]: return fold_tree(t, f_leaf3, f_node3) def mirror_as_fold(t : Tree) -> Tree: return fold_tree(t, f_leaf4, f_node4)
Again just using
fold_tree
and without making use of any auxiliary recursion, define a functioneq_tree(t: Tree, u: Tree) -> bool
that determines whether or not two treest
andu
are structurally equal.Examples:
13]: example1 == Node(Leaf(0), Node(Leaf(1), Leaf(2))) In [13]: False Out[ 14]: eq_tree(example1, Node(Leaf(0), Node(Leaf(1), Leaf(2)))) In [14]: True Out[ 15]: eq_tree(example2, Node(Leaf(0), Node(Leaf(1), Leaf(2)))) In [15]: False Out[
Hint: it may help to first define
eq_tree
using recursion and pattern-matching, before trying to define it as a fold. It is important to keep in mind thatfold_tree
can compute a value of arbitrary type, in particular it can return a function as its answer.
Rémy’s algorithm
Rémy’s algorithm is a simple and elegant iterative algorithm for generating random binary trees of a fixed size. It is a uniform generator in the sense that every binary tree is generated with equal probability.
At a high level, the algorithm can be described as follows:
Start: a tree with one leaf with label 0.
For i from 1 to n:
Choose one existing subtree u at random. (There are 2n+1 subtrees when the tree has n internal nodes.)
Toss a coin.
If Heads, make u the left child of a new internal node; attach a new leaf with label i as the right child of the new internal node.
If Tails, do the symmetric thing: u goes on the right and the new leaf with label i goes on the left of a new internal node.
Here is a little animation of Rémy’s algorithm being used to generate a random binary tree with 16 leaves, followed by running the algorithm in reverse to deconstruct the tree:
Observe that as we grow new leaves, we also label them in the order of creation.
Write a function
remy(n : int) -> Tree
which uses Rémy’s algorithm to generate a uniformly random binary tree with \(n\) binary nodes and \(n+1\) leaves, with the leaves labelled \(0,\dots,n\) in order of creation.Examples:
16]: remy(8) In [16]: Node(Node(Node(Node(Node(Leaf(7), Node(Leaf(3), Leaf(1))), Node(Leaf(2), Leaf(6))), Leaf(5)), Leaf(8)), Node(Leaf(0), Leaf(4))) Out[ 17]: remy(8) In [17]: Node(Leaf(1), Node(Leaf(2), Node(Node(Leaf(0), Leaf(5)), Node(Node(Leaf(8), Leaf(4)), Node(Leaf(7), Node(Leaf(6), Leaf(3))))))) Out[
Hint: to facilitate the step of picking a random subtree
u
oft
and replacing it by a larger treeNode(Leaf(k), u)
orNode(u, Leaf(k))
, begin by writing a function that enumerates every subtree together with a pointer from its immediate parent. More precisely, write a generator functionsubtrees(t: Tree, pt: Node, lct: bool) -> Iterator[tuple[Tree, Node, bool]]
with the following specification:subtrees(t, pt, lct)
generates all triples(u, p, lc)
such thatu
is a subtree oft
,p
is the immediate parent ofu
int
, andlc
isTrue
just in caseu
is the left child ofp
, under the assumption thatt = pt.left
iflct = True
, andt = pt.right
iflct = False
. Then you can writeremy(n)
by repeatedly callingsubtrees
and updating the tree appropriately. To simplify the code, it may help to begin by creating a tree with a dummy binary node at the root and a leaf labelled 0 as (say) its right child.