You are on page 1of 50

Summary

Data Abstraction – Lists and Dictionary


Orders of Growth
Abstract Data Types (ADTs)
Binary Trees
Stacks and Queues
Higher Order Procedures (HOPs)
HOP with Lists
HOPs with Map, Filter, Folding
Mutables vs Immutables
Kinds of Data Structures
Basically two kinds of data structures:
 Built-in data structures
 User-defined data structures

Python comes with a general set of built-in data


structures:
 Strings, Lists, Tuples, Dictionaries
Traversing a List
 Most common way to traverse the elements of a list is with a for loop. For
example to a function sumlist which sums all elements in a list would be
given by:

def sumlist(lst):
total = 0
for i in lst:
total=total + i
return total

>>>sumlist([1,2,3,4])
10
List Comprehension
 One way is a “list comprehension”
[n for n in range(1,5)]

returns mark the comp with [ ]


[1,2,3,4] [ n for n in range(1,5) ]

what we what we iterate


collect through. Note that
we iterate over a set of
values and collect some
(in this case all) of them
Multiple and Conditional Collects
[(x,y) for x in range(1,4) for y in range (1,4)]
=> [(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]

[x for x in range(1,30) if x%2==0]

 The “if” part of the comprehensive controls which of the iterated values is
collected at the end.
 Only those values which make the if part true will be collected:

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
Python Dictionary
 Use the { } marker to create a dictionary
 Use the : marker to indicate key:value pairs:

contacts =
{'bill': '353-1234',
'rich': '269-1234',
'jane': '352-1234'}
Dictionary: Keys and Collections
 Key must be immutable:
- strings, integers, tuples are fine
- lists are NOT
 Dictionaries are collections,but they are not sequences like
lists, strings or tuples:
- there is no order to the elements of a
dictionary
- in fact, the order might change as elements
are added or deleted.
Access Dictionary Elements
 Access requires [ ], but the key is the index!
myDict={}
- an empty dictionary
myDict['bill']=25
- added the pair ‘bill’:25’
print(myDict['bill'])
- prints the value matching to key ‘bill’
myDict['bill'] = 100
- changes the value matching to key ‘bill’
for k in myDict: print(k)
- prints all the keys
for k,v in myDict.items(): print (k,v)
- prints all the key/value pairs
Building Dictionaries Faster
 zip creates pairs from two parallel lists:
zip("abc",[1,2,3]) yields
[('a',1),('b',2),('c',3)]

 That’s good for building dictionaries. We call the dict


function which takes a list of pairs to make a dictionary:
dict(zip("abc",[1,2,3])) yields
{'a': 1, 'c': 3, 'b': 2}
Characterising Resource Usage
– Orders of Growth

eg.
fib2
eg.
fib
eg.
fib5
(even)
Data Abstractions

Abstractions separate implementation of data representation from its
use.
– Functions built on top of points and segments do not access the
underlying tuples directly.
– If implementation changes, those functions do not have to
change


Data Abstractions (like procedural abstractions) allow us to build
up complex objects gradually.
ADTs

ADT = Abstract Data Type

A Methodology for constructing our own compound
data

Ingredients:
– Concrete representation for our concept
– Functions to manipulate that representation
ADT Functions: Classification

ADT Functions are classified according to their purpose:

Constructors: create an instance of our conceptual object
(e.g. makePoint)

Selectors: return components of the object e.g. xCoord

Predicates: test object for properties, return True/False

Mutators: modify an object already in existence
ADT Example:
Binary Tree
 Recursive like a list, but with 2 recursive parts:
 A binary tree is either
 empty OR
 a node + 2 binary trees
 (Need empty tree to make a non-empty tree)
root

empty
tree left right
subtree subtree
Binary Tree ADT
 Constructors:
 makeEmptyTree: void → Btree
 makeTree: (A, Btree<A>, Btree<A>) → Btree<A>
 Selectors:
 root: Btree<A> → A
 leftSubtree: Btree<A> → Btree<A>
 rightSubtree: Btree<A> → Btree<A>
 Predicate:
 isEmptyTree: Btree<A> → Bool
Binary Tree Contract
isEmptyTree(makeEmptyTree()) → True

root(makeTree(6, makeEmptyTree(), makeEmptyTree())) → el

leftSubtree(makeTree(el, left, right)) → left

rightSubtree(makeTree(el, left, right)) → right


Binary Tree Example
t = makeTree(
5,
makeEmptyTree(),
makeEmptyTree()
)

5
Binary Tree Example
5
t = makeTree(
5,
makeTree(
7 11 7,
makeEmptyTree(),
makeTree(
13 13,
makeEmptyTree(),
makeEmptyTree()
)
),
makeTree(
11,
makeEmptyTree(),
makeEmptyTree()
)
Binary Tree Example
 root (t)
5
 Extract 7 from t?
root(leftSubtree(t))
 Extract 13 from t?
root(rightSubtree(leftSubtree(t)))
 Extract 11 from t?
root(rightSubtree(t))
(FIFO) Queue
 FIFO ordering of elements
 Used to support:
 graph traversal (breadth-first)
 game playing (iterated deepening)
 event handling
enqueue dequeue

back front
Implementing the Queue ADT:
The Constructors
 makeQueue: void → Queue
def makeQueue():
return ('queue', [])
def contents(q):
return q[1]

 Good to have a predicate that can detect valid queues:


def isQueue(obj):
return type(obj) == type(()) and \
obj[0] == 'queue'
Implementing the Queue ADT:
The Mutators
 enqueue: (Queue<A>, A) → void
def enqueue(q,el):
#adding to the back
if isQueue(q):
contents(q).append(el)
else:
#raise TypeError, "enqueue : Not a Queue"
return TypeError (q, "enqueue : Not a Queue")
 dequeue: (Queue<A>) → void
def dequeue(q):
#removing from front
if not isQueueEmpty(q):
contents(q).pop(0)
else:
raise IndexError ("Queue is Empty")
Implementing the Queue ADT:
The Selector and Predicate
 front: Queue<A> → A
def front(q):
if isQueue(q) and not isQueueEmpty():
return contents(q)[0]
else:
raise TypeError("dequeue: arg must be a queue")
 isQueueEmpty: Queue<A> → Bool
def isQueueEmpty(q):
if isQueue(q):
return contents(q) == []
else:
raise TypeError ("arg must be a queue")
(LIFO) Stack
 LIFO ordering of elements
 Used to support:
 recursion
 parsing
 graph traversals (depth-first)
 game playing

top
Implementing the Stack ADT:
The Constructor and Predicate
def makeStack():
return ('stack', [])

def contents(s):
return s[1]


Good to have a predicate that can detect valid stacks:
def isStack(obj):
return type(obj) == type(()) and \
obj[0] == 'stack'
Implementing the Stack ADT:
The Mutators
 push: Stack<A>. <A> → void
def push(s, el):
if isStack(s):
contents(s).insert(0, el)
else:
raise TypeError("push: First arg must be a stack")
 pop: Stack<A> → void
def pop(s):
if isStack(s) and not isStackEmpty(s):
contents(s).pop(0)
else:
raise TypeError("pop: arg must be a stack")
Implementing the Stack ADT:
The Selector
 top: Stack<A> → <A>

def top(s):
if isStack(s) and not isStackEmpty(s):
return contents(s)[0]
else:
raise TypeError("top: arg must be a stack")
Implementing the Stack ADT:
The Predicate (review)
 isStackEmpty: Stack<A> → Bool

def isStackEmpty(s):
if isStack(s):
return contents(s) == []
else:
raise TypeError("isStackEmpty: arg must be a stack")
Procedural Abstraction
 1 + 2 + 3 + 4 + 5 = 15
 12 + 22 + 32 + 42 = 30
 1/12 + 1/32 + 1/52 + ... + 1/1012 ≈ π2/8

 Common pattern is summing from some start point to some


termination point.
 We could write a procedure to capture this, for each of the
sums
Higher Order Procedures
def sum(term, start, next, stop):
if start > stop:
return 0
else:
return term(start) + \
sum(term, next(start), next, stop)
def inc(n):
return n + 1
def sumSquares(start, stop):
if start > stop:
>>> sum(sq, 1, inc, 4) return 0
else:
30 return sq(start) + \
sumSquares(start + 1, stop)
Anonymous Functions
 Functions do not have to have names
 Use keyword lambda to introduce an anonymous function
lambda n: n + 1
a function that accepts an input n and returns the value
n+1
In Mathematics: n → n + 1
Anonymous Functions mix well
with Higher Order Procedures
def sum(term, start, next, stop):
if start > stop:
def inc(n): return 0
return n + 1 else:
def sq(n): return term(start) + \
return n * n sum(term, next(start),
next, stop)

>>> sum(sq, 1, inc, 4)


30

>>> sum(sq, 1, lambda n: n+1, 4)


30

>>> sum(lambda n: n*n, 1, lambda n: n+1, 4)


30
Using Higher Order Procedures
 A higher order procedure is a procedure that either accepts a
procedure as an argument, or returns one as its result.
 Higher Order Procedures are generalisations
 Each specific process becomes a special case, invoked by passing in
appropriate parameters
 Arguments that are functions alter the behaviour of the overall
process
Reimplementing the Examples

 1 + 2 + 3 + 4 + 5 = 15
sum(lambda n: n,1,lambda n: n+1,5)

 12 + 22 + 32 + 42 = 30
sum(lambda x: x*x, 1, lambda x: x+1, 4)

def sum(term, start, next, stop):


.
.
.
Reimplementing the Examples
 1/12 + 1/32 + 1/52 + ... + 1/1012 = π2/8

sum(lambda n: 1.0/(n*n),1,lambda n: n+2, 101)

OR
sum(sqReciprocal, 1, lambda i: i+2, 101)

NB: lambda i: i + 2 is the same function, as


lambda n: n +def2 sum(term, start, next, stop):
.
.
The name of the parameter does not matter.
.
HOPs with Lists
 Processing a list often involves:
 proceeding one element at a time,
 performing some operation for each element
 combining the results in some way
 HOPs take care of proceeding one element at a time.
 We now focus on the other two issues
 We may build HOP operations (map, filter, foldr) to represent
this idea generically.
Function map() Explained
Format of usage of Python map()
A function name or a defined function

map ( function, list )

A list or name of a list


keyword

e.g.
map (lambda n: n**2, [1,2,3,4,5])
map (square, lst)
Generalising: map
Procedure my_map (because map is already a pre-defined function) takes
a function and a list as arguments and produces a list containing the
result of applying that function to each of the elements of the list:
e.g.
my_map(lambda x: x*2, [1,2,3]) => [2, 4, 6]

Function defined using list comprehension -


def my_map(f,lst):
return [f(i) for i in lst]
Using map
Function defined recursively -
def my_map(f,lst):
if lst == []:
return []
else:
return [f(lst[0])] + my_map(f, lst[1:])

def list_copy(lst):
return my_map(lambda x: x, lst)
def list_double(lst):
return my_map(lambda x: x*2, lst)
def list_square(lst):
return my_map(lambda x: square(x), lst)
Examples: map
def sq(x): return x * x

>>> lst = [3, 5, 7, 9, 11, 13, 17]

>>> my_map(sq, lst)


[9, 25, 49, 81, 121, 169, 289]

>>> map(sq, lst)


<map object at 0x02E04330>

>>> list(map(sq, lst))


[9, 25, 49, 81, 121, 169, 289]
filter: when you don't want all
elements of a list
 filter takes a predicate and a list as arguments, and returns a new
list containing only those elements of the given list for which the
predicate is true.

def my_filter(predicate, lst):


if lst == []:
return []
elif predicate(lst[0]):
return [lst[0]] + my_filter(predicate, lst[1:])
else:
return my_filter(predicate, lst[1:])
filter: when you don't want all
elements of a list
 filter takes a predicate and a list as arguments, and returns a new
list containing only those elements of the given list for which the
predicate is true.
>>>lst = [3, 17, 11, 9, 7, 2, 19, 17]

>>> my_filter(lambda x: x > 10, lst)


[17, 11, 19, 17]

>>> filter(lambda x: x > 10, lst)


<filter object at 0x02E044B0>

>>> list(filter(lambda x: x > 10, lst))


[17, 11, 19, 17]
list comprehension
 list comprehension provides an alternative to making direct calls to
map and filter:
>>>lst = [3, 17, 11, 9, 7, 2, 19, 17]

>>> my_filter(lambda x: x > 10, lst)


[17, 11, 19, 17]
>>> [x for x in lst if x > 10]
[17, 11, 19, 17]

>>> my_map(lambda x: 2*x, lst)


[6, 34, 22, 18, 14, 4, 38, 34]
>>> [2*x for x in lst]
Folding a List
 Folding combines the elements of a list to a single value.
e.g. listSum ([2, 3, 5, 7, 11]) → 28
e.g. listProd ([5, 5, 7]) → 175
e.g. listMax ([2, 5, 3, 11, 7, 4]) → 11

 We can write one HOP to generalise listSum, listProd, and


listMax.
 Differences:
base case value, combining operation
 Capture differences with parameters
foldr: Implementation
def foldr(combiner, base, lst):
if lst == []:
return base
else:
return combiner(lst[0],foldr(combiner, base, lst[1:]))

>>> foldr (lambda x,y: x+y, 0, [1,2,3,4,5])


=> 1 + (2 + (3 + (4 + (5 + 0))))
15

>>> foldr (lambda x,y: x*y, 1, [1,2,3,4])


=> 1 * (2 * (3 * (4 * 1)))
24

>>> foldr (lambda x,y: x/y, 10, [1,2,3])


=> 1 / (2 / (3 / 10))
0.15
Mutable vs Immutable
 If two variables
associate with the
same object, then
both reflect any
change to that object.
Split
 The string method split generates a sequence of characters
by splitting the string at certain split-characters.
 It, too, returns a list:

>>> splitLst = 'this is a test'.split()


>>> splitLst
['this', 'is', 'a', 'test']
Immutables
 Assignment takes an object
from the RHS and associates
it with a variable on the LHS.
 When you assign one
variable to another, you
share the association with
the same object.
 With immutables, any
changes that occur generate a
new object.
Change an Object’s Contents
 Strings and tuples are immutable.
 Once created, the object’s contents cannot be changed. New
objects can be created to reflect a change, but the object itself
cannot be changed:

myStr = 'cat' # can do


myStr[0] = 'b' # cannot do!

• Tuples are immutable lists


Summary
Data Abstraction – Lists and Dictionary
Orders of Growth
Abstract Data Types (ADTs)
Binary Trees
Stacks and Queues
Higher Order Procedures (HOPs)
HOP with Lists
HOPs with Map, Filter, Folding
Mutables vs Immutables

You might also like