Sequence operations
In the following exercises, you can treat sequences as lists or tuples, unless explicitly stated otherwise.
Define a function mysum(seq) that returns the sum of all numbers in the list seq.
def mysum(seq):
"""Return the sum of all numbers in seq.
In:
seq - a sequnce of summable items
Out:
sum of all items in seq
"""
Solutions
With while:
def mysum(seq):
total = 0
while seq:
total += seq[0]
seq = seq[1:]
return total
With for
def mysum(seq):
total = 0
for x in seq:
total += x
return total
With proc:
from funcutils import proc
mysum = lambda seq: proc((0,seq),
lambda s: s[1],
lambda s: (s[0]+s[1][0], s[1][1:]))[0]
or
mysum = lambda seq: proc([0]+seq, lambda s: len(s)>1, lambda s: [s[0]+s[1]]+s[2:])[0]
With funcutils.reduce, which is I think as short as you get:
from functools import reduce
from operator import add
mysum = lambda seq: reduce(add, seq)
Another thing that the reduce solution is superior to the others, including Python’s built-in sum, is that it can project the generality of addition. You cannot find the “sum” of a list of lists with sum, but with mysum defined with reduce you can do it:
>>> mysum([[1,2],[3],[4,6,7]])
[1, 2, 3, 4, 6, 7]
One drawback is that reduce-based solution cannot handle empty sequences. For that, you need to specify 0 as a third argument for arithmetic addition and [] for list concatenation.
Library
x = [1,2,3,4]
sum = sum(x)
print(sum) # Output: 10
Define a function myinsert(seq,index,item) that inserts item at position index of the list seq and returns the new list.
def myinsert(seq, index, item):
"""Insert item at index of seq.
>>> myinsert([1, 2, 3], 1, 4)
[1, 4, 2, 3]
>>> myinsert([1, 2, 3], 5, 4)
[1, 2, 3]
>>> myinsert([True, False], 2, None)
[True, False, None]
>>> myinsert([],0,1)
[1]
>>> myinsert([1,4],1,[2,3])
[1, [2, 3], 4]
"""
Define a function myreverse that reverses a given list. Write 3 versions: using while,for, and proc from funcutils.
Define a function takewhile(alive, seq) that takes elements from the front of
the sequence seq as long as the predicate function alive returns True. The
function should return a new list containing the taken elements.
Also define the dropwhile(alive, seq) variant that drops elements from the front of the
sequence as long as alive returns True, returning the
remaining elements as a new list.
def takewhile(alive, seq):
"""Take elements from seq while alive returns True.
>>> takewhile(lambda x: x < 3, [1, 2, 3, 4, 1])
[1, 2]
>>> takewhile(lambda x: x != 0, [1, 2, 3])
[1, 2, 3]
>>> takewhile(lambda x: x > 0, [-1, 2, 3])
[]
>>> takewhile(lambda x: True, [1, 2, 3])
[1, 2, 3]
"""
Solutions
With proc:
def takewhile(alive, seq):
return proc(([],seq),
lambda s: s[1] and alive(s[1][0]),
lambda s: (s[0] + [s[1][0]], s[1][1:]))[0]
Library
itertools.takewhile
Define a function mymap(func, seq) that applies the function func to each
element of the sequence seq and returns a new list containing the results.
def mymap(func, seq):
"""Map func over seq.
>>> mymap(lambda x: x * 2, [1, 2, 3])
[2, 4, 6]
>>> mymap(str, [1, 2, 3])
['1', '2', '3']
"""
Criteria
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # Output: [1, 4, 9, 16, 25]
Define a function myfilter(test, seq) that removes items in seq that return False for test.
def myfilter(test, seq):
"""Filter seq by test.
>>> myfilter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5, 6])
[2, 4, 6]
"""
Notes
- Solve it with
whileand also withproc_seqorprocfrom funcutils. - The sequence can be implemented as a
tupleor alist. - If you use
tuples, you can only manipulate them using slicing and concatenation (+). - Lists allow for in-place manipulation.
- You can try versions with in-place manipulation (for list based solutions), but make sure you can solve the problem wihhout in-place manipulation.
Library
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers) # Output: [2, 4, 6]
Define a function myreduce(seq, func, initial) that reduces the elements of seq using the binary function func, starting with the value initial.
def myreduce(func, seq, initial):
"""Aggregate elements of seq using func starting with initial.
>>> myaggregate(lambda x, y: x + y, [1, 2, 3], 0)
6
>>> myaggregate(lambda x, y: x * y, [1, 2, 3, 4], 1)
24
>>> myaggregate(lambda x, y: x + y, [], 10)
10
>>> myaggregate(lambda x, y: x - y, [2, 3, 4], 20)
11
"""
Library
from functools import reduce
numbers = [1, 2, 3, 4]
total = reduce(lambda x, y: x + y, numbers, 0)
print(total) # Output: 10
Define a function myenumerate(seq) that takes a sequence seq and returns a
list of tuples, where each tuple contains an index and the corresponding element
from the sequence.
def myenumerate(seq):
"""Enumerate seq into a list of (index, item) tuples.
>>> myenumerate(['a', 'b', 'c'])
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> myenumerate([10, 20, 30, 40])
[(0, 10), (1, 20), (2, 30), (3, 40)]
"""
Library
items = ['a', 'b', 'c']
enumerated_items = list(enumerate(items))
print(enumerated_items) # Output: [(0, 'a'), (1, 'b'), (2, 'c')]
Define a function myzip(seq1, seq2) that takes two sequences seq1 and seq2 and returns a list of tuples, where each tuple contains one element from seq1 and the corresponding element from seq2. The length of the returned list should be equal to the length of the shorter input sequence.
def myzip(seq1, seq2):
"""Zip seq1 and seq2 into a list of tuples.
>>> myzip([1, 2, 3], ['a', 'b', 'c'])
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> myzip([1, 2], ['a', 'b', 'c'])
[(1, 'a'), (2, 'b')]
>>> myzip([], [1, 2, 3])
[]
>>> myzip([1, 2, 3], [])
[]
"""
Library
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
zipped = list(zip(list1, list2))
print(zipped)
Define a function shuffle(seq) that returns a randomly shuffled version of the input sequence seq without modifying the original sequence. Use the random module.
import random
def shuffle(seq):
"""Return a randomly shuffled version of seq.
>>> shuffle([1, 2, 3])
[2, 1, 3] # example output; actual output will vary
>>> shuffle(['a', 'b', 'c', 'd'])
['c', 'a', 'd', 'b'] # example output; actual output will vary
"""
Notes and criteria
- You cannot use any built-in randomization functions that directly shuffle or sample from sequences.
- You are allowed to use the
randommodule to generate random numbers. - All auxiliary functions, e.g. for removing an element from a sequence, must be implemented by yourself.
- Solve it with
whileand withprocfrom funcutils.
Solutions
With proc:
def shuffle(seq):
"""Non-mutating shuffle of seq"""
from funcutils import proc
from random import randint
return proc(([], seq),
lambda s:s[1],
lambda s: (lambda r:
(s[0]+[s[1][r]], s[1][0:r]+s[1][r+1:]))
(randint(0, len(s[1])-1)))[0]
The above code is cryptic on purpose. If you can work out how my update function works, you are getting well adapted to functional programming. With a separate remove_at function like:
remove_at = lambda seq, index: seq[0:index] + seq[index+1:]
the update function becomes a little clearer:
lambda s: (lambda r:
(s[0]+[s[1][r]], remove_at(s[1], r)))
(randint(0, len(s[1])-1))
But still the lambda-in-lambda may be perplexing. We will deal with this kind of constructs later. You can simply use def:
def update(s):
r = randint(0, len(s[1])-1)
return (s[0]+[s[1][r]], remove_at(s[1], r))
Library
import random
items = [1, 2, 3, 4, 5]
shuffled_items = random.sample(items, len(items))
print(shuffled_items) # Output: A randomly shuffled version of items
Define a function freqcount(seq) that takes a sequence seq and returns a
dictionary mapping each unique element in the sequence to its frequency of
occurrence.
def freqcount(seq):
"""Count frequencies of items in seq.
>>> freqcount(['a', 'b', 'a', 'c', 'b', 'a'])
{'a': 3, 'b': 2, 'c': 1}
>>> freqcount([1, 2, 2, 3, 1, 1, 4])
{1: 3, 2: 2, 3: 1, 4: 1}
"""
Solutions
A non-mutating solution with reduce would look like:
from functools import reduce
def update_count(fdict, item):
"""Update the frequency dictionary with the given item."""
return fdict | {item: fdict.get(item, 0) + 1}
def compute_frequencies(token_list):
"""Compute frequency distribution of tokens in the list."""
return reduce(update_count, token_list, {})
The problem with this solution is that as the update_count function updates
the dictionary by creating a new dictionary each time, it is not very efficient.
One remedy for this efficiency problem is to mutate the frequency dictionary in place:
def update_count(fdict, item):
"""Update the frequency dictionary with the given item."""
fdict[item] = fdict.get(item, 0) + 1
return fdict
def compute_frequencies(token_list):
"""Compute frequency distribution of tokens in the list."""
return reduce(update_count, token_list, {})
If we want to avoid the assignment statement in update_count, we need to
switch to a third-party library for persistent data structures that make
functional updates efficient. Here is a solution with immutables.Map:
from immutables import Map
def update_count(fdict, item):
"""Update the frequency dictionary with the given item."""
return fdict.set(item, fdict.get(item, 0) + 1)
def compute_frequencies(token_list):
"""Compute frequency distribution of tokens in the list."""
return reduce(update_count, token_list, Map())
Library
from collections import Counter
def freqcount(seq):
"""Count frequencies of items in seq.
return dict(Counter(seq))