Lecture 12

Today:

  • More Recursion(Recursion(Recursion(...)))
  • Functional programming tools in Python: lambda, filter, map, reduce

Zip

In [1]:
list(zip([1,2,3,4], ["a","s","d","f"]))
Out[1]:
[(1, 'a'), (2, 's'), (3, 'd'), (4, 'f')]


A very good recursion example

Flattening lists:

Let us write a function that will flatten a list. I.e. it takes a list of lists, and returns a list containing all the elements that it sees in any sublists.

for example: flatten([1,2,[1,2,3,4],[1,[2,[5,6]]]]) should return [1,2,1,2,3,4,1,2,5,6].

When there is recursion, there is always a basic idea you can write down, like this: the flattening of a list is the flattening of the first element of the list concatenated with the flattening of the rest of the list. so we could say something like: flatten(xs) = flatten(xs[0]) + flatten(xs[1:])

In [2]:
def flatten(xs):
    return flatten(xs[0]) + flatten(xs[1:])
In [3]:
flatten([1,2,[3,4]])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-bb45049d2a89> in <module>()
----> 1 flatten([1,2,[3,4]])

<ipython-input-2-675becf1f374> in flatten(xs)
      1 def flatten(xs):
----> 2     return flatten(xs[0]) + flatten(xs[1:])

<ipython-input-2-675becf1f374> in flatten(xs)
      1 def flatten(xs):
----> 2     return flatten(xs[0]) + flatten(xs[1:])

TypeError: 'int' object is not subscriptable

What happened? Where is the mistake? The mistake is that we are calling flatten(xs[0]), but xs[0] might not be a list, e.g. xs[0] might be a number, in which case we want flatten(5) to return 5. To check whether something is a list, you use isinstance.

In [4]:
def flatten(xs):
    if isinstance(xs, list):
        return flatten(xs[0]) + flatten(xs[1:])
    return [xs]
In [5]:
flatten([1,2,[3,4]])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-5-bb45049d2a89> in <module>()
----> 1 flatten([1,2,[3,4]])

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

<ipython-input-4-2e492067175e> in flatten(xs)
      1 def flatten(xs):
      2     if isinstance(xs, list):
----> 3         return flatten(xs[0]) + flatten(xs[1:])
      4     return [xs]

IndexError: list index out of range

more mistakes?!?!! what is the problem? It's that we are calling flatten[xs[1:]] when xs is a list of length 1, so there is no xs[1].

In [6]:
def flatten(xs):
    if isinstance(xs, list):
        if len(xs)>0:
            return flatten(xs[0]) + flatten(xs[1:])
        else:
            return []
    return [xs]
In [7]:
flatten([1,2,[3,4]])
Out[7]:
[1, 2, 3, 4]
In [8]:
flatten([1,2,[3,4,[1,2,1,2,2,[9,9]]]])
Out[8]:
[1, 2, 3, 4, 1, 2, 1, 2, 2, 9, 9]

It works!

Lambda, map, reduce, filter

These expressions/functins allow you to do a lot using lists. Expecially with one-liners.

Especially lambda is used quite often. map, reduce and filter are not used very often. BUT: it's important for us that we learn the ideas that they represent. That's why we will do exercises with them.

Lambda expressions

A lot of times we write functions like:

def f(x,y,...):
    return ...some_expression_using_x_y_...

There is a nice shortcut for this.

lambda x,y,... : ...some_expression_using_x_y_...
In [9]:
f = lambda x: x*x
f(5)
Out[9]:
25
In [10]:
(lambda x,y : x + y)(2,5)
Out[10]:
7

lambda comes from Lambda Calculus, which was a model of computation that mathematicians thought about before computers existed. It was invented by Alonso Church (who was the advisor of Alan Turing)

Map

Say I want to apply a function to every element in a list.

map(f,xs), returns: [f(xs[0]), f(xs[1]),...]. i.e. it applies f to each element of xs.

In [11]:
from math import floor
list(map(floor, [1.234, 2.1234145, 3.42424, 4.525]))
Out[11]:
[1, 2, 3, 4]

Remark: In Python 2, this was just map, but in Python 3, we have to se list(map(...)) to get a list.

In fact, this is the same as using list comprehension:

In [12]:
[floor(x) for x in [1.234, 2.1234145, 3.42424, 4.525]]
Out[12]:
[1, 2, 3, 4]

We won't be using map much and will use list comprehension instead. I did use map in the past for parallel computing. If you call map(f,xs), then Python knows that f(xs[0]), f(xs[1]),... are all separate computations that don't depend on each other. So it can run each computation in parallel on separate cores of your processor (if you ask it to do so). This is called multithreading.

When used, map is often combined with lambda.

In [13]:
list(map(lambda x: 2**x, range(15)))
Out[13]:
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]

This would be much nicer with list comprehensions but still good to know.

In [14]:
[2**x for x in range(15)]
Out[14]:
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]

Filter

filter(f, xs) returns a list containing the elements of xs for which f returns True (or something else Python interprets as True)

In [15]:
# our trusty old isprime
# note that isprime returns bool
def isprime(n):
    if n <= 1:
        return False
    d = 2
    while d*d<=n:    # so clever!!!
        if n % d  == 0:
            return False
        d += 1
    return True
In [16]:
list(filter(isprime, range(20)))
Out[16]:
[2, 3, 5, 7, 11, 13, 17, 19]

But again we already know how to do this:

In [17]:
[x for x in range(20) if isprime(x)]
Out[17]:
[2, 3, 5, 7, 11, 13, 17, 19]

Reduce

This is the one that's really new for us because it makes very nice one-liners, but it's in a library called functools. (it used to be standard in Python 2)

reduce(f, xs), takes a function f(x,y) of two variables, and applies the function first to x[0] and x[1], getting f(x[0], x[1]). And then applies f to f(x[0], x[1]) and x[2], getting f(f(x[0], x[1]), x[2]),...

In [18]:
from functools import reduce
reduce(lambda x, y: x+y, range(10))
Out[18]:
45

Let's see what happened:

lambda x, y: x+y is the addition function. range[10] is [0,1,2,3,4,5,6,7,8,9]

reduce(lambda x, y: x+y, range(10)) first computes 0+1, takes the result 1 and adds it to 2, then takes the result 3 and adds it to the next element 3.

In [19]:
reduce(lambda x, y: x*y, range(1,10))
Out[19]:
362880

$362880$ is $10!$


In [20]:
factorial = lambda n: reduce(lambda x, y : x*y, range(1,n))
In [21]:
factorial(10)
Out[21]:
362880

Do you remember the homework problem when we were supposed to write a function dupli(xs,k) that takes a list and returns the same list but with each element repeated k times:

In [22]:
xs = [1,2,4,1,2]
diplo = lambda xs, k: reduce(lambda x, y: x+y, map(lambda a: k*[a], xs))

# this is clearly not a good way to implement this function!! It's hard to read. 
# still, it's cool that we can do this and it's good exercise for the brain
diplo(xs, 3)
Out[22]:
[1, 1, 1, 2, 2, 2, 4, 4, 4, 1, 1, 1, 2, 2, 2]

Exercises

  • Using map and lambda, write a function that will take the square of each number in a list.
  • Using the above function and reduce, write a function that will return the Euclidean norm of a list/tuple. (i.e. $\sqrt{x_1^2 + ... + x_n^2}$)
  • Hard: What is the function apply(n,f,x) below doing?
In [23]:
apply = lambda n,f,x : reduce((lambda y, g : g(y)),([x] + n*[f]))
In [24]:
apply(1, lambda x: x+1, 1)
Out[24]:
2
In [25]:
apply(3, lambda x: x*x, 2)
Out[25]:
256