PyCon India 2011
    
    September 16, 2011
With lot of interesting examples!
>>> def square(x):
...     return x *x
... 
>>> square(4)
16
Functions can be assigned like any other objects.
>>> f = square
>>> f(4)
16
They can be passed as arguments to other functions.
>>> def fsum(f, x, y):
...     return f(x) + f(y)
... 
>>> fsum(square, 3, 4)
25
Anonymous functions can be created using the lambda operator.
>>> fsum(lambda x: x*x, 3, 4)
25
Functions can even return new functions. We'll see that later.
Python has built-in support for lists.
>>> [1, 2, 3, 4]
[1, 2, 3, 4]
>>> ["hello", "world"]
["hello", "world"]
>>> [0, 1.5, "hello"]
[0, 1.5, "hello"]
A List can contain another list as member.
>>> a = [1, 2]
>>> b = [1.5, 2, a]
>>> b
[1.5, 2, [1, 2]]
A list can contain even it self.
>>> a = [1, 2]
>>> a.append(a)
>>> a
[1, 2, [...]]
Some operations on lists.
>>> x = [5, 2, 3, 1]
>>> len(x)
5
>>> 2 in x
True
>>> 4 in x
False
>>> x.sort()
>>> x
[1, 2, 3, 5]
The sorted function returns a new sorted list instead of modifying the original.
>>> x = [5, 2, 3, 1]
>>> sorted(x)
[1, 2, 3, 5]
The sorted function and sort method can take a key function as argument.
>>> x = ["a", "c", "D", "B"]
>>> sorted(x)
['B', 'D', 'a', 'c']
>>> sorted(x, key=lambda a: a.lower())
['a', 'B', 'c', 'D']
Dictionaries are like lists, but they can be indexed with non integer keys also. Unlike lists, dictionaries are not ordered.
>>> a = {'x': 1, 'y': 2, 'z': 3}
>>> a['x']
1
>>> a['z']
3
>>> a['zz']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'zz'
>>> a.get('zz', 0)
0
The in operator can be used to check if a key is present in the dictionary.
>>> 'z' in a
True
>>> 'zz' in a
False
The del operator can be used to delete an item from a dictionary.
>>> del a['x']
>>> a
{'y': 2, 'z': 3}
>>> 'x' in a
False
Compute frequency of words in a given file.
Lets start with a function to count frequency of words, given a list of words.
def word_frequency(words):
    """Returns frequency of each word given a list of words.
        >>> word_frequency(['a', 'b', 'a'])
        {'a': 2, 'b': 1}
    """
    frequency = {}
    for w in words:
        frequency[w] = frequency.get(w, 0) + 1
    return frequency
Getting words from a file is very easy.
def read_words(filename):
    return open(filename).read().split()
We can combine these two functions to find frequency of all words in a file.
def main(filename):
    words = read_words(filename)
    frequency = word_frequency(words)
    for word, count in frequency.items():
        print word, count
if __name__ == "__main__":
    import sys
    main(sys.argv[1])
Write a program to count frequency of characters in a given file.
Write a function invertdict to interchange keys and values in a dictionary.
  For simplicity, assume that all values are unique.
>>> invertdict({'x': 1, 'y': 2, 'z': 3})
{1: 'x', 2: 'y', 3: 'z'}
Write a program to find anagrams in a given list of words. Two words are called anagrams if one word can be formed by rearranging letters of another. For example "eat", "ate" and "tea" are anagrams.
>>> anagrams(['eat', 'ate', 'done', 'tea', 'soup', 'node'])
[['eat', 'ate', 'tea], ['done', 'node'], ['soup']]
List Comprehensions provide a concise way of creating lists.
Here are some simple examples for transforming a list.
>>> a = range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [x for x in a]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [x*x for x in a]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
It is also possible to filter a list using if condition inside a list comprehension.
>>> a = range(10)
>>> [x for x in a if x%2 == 0]
[0, 2, 4, 6, 8]
>>> [x*x for x in a if x%2 == 0]
[0, 4, 8, 36, 64]
We can even use multiple fors in single list comprehension.
>>> [(x, y) for x in range(5) for y in range(x) if (x+y)%2 == 0]
[(2, 0), (3, 1), (4, 0), (4, 2)]
Same as above, indented for clarity.
>>> [(x, y) 
...     for x in range(5) 
...     for y in range(x) 
...     if (x+y)%2 == 0]
...
[(2, 0), (3, 1), (4, 0), (4, 2)]
Write a function mutate to compute all words generated by a single mutation on
a given word. A mutation is defined as inserting a character, deleting a
character, replacing a character, or swapping 2 consecutive characters in a
string. For simplicity consider only letters from a to z.
>>> words = mutate('hello')
>>> 'helo' in words
True
>>> 'yello' in words
True
>>> 'helol' in words
True
Write a function nearly_equal to test whether two strings are nearly equal.
Two strings a and b are nearly equal when a can be generated by a single
mutation on b.
>>> nearly_equal('python', 'perl')
False
>>> nearly_equal('python', 'jython')
True
>>> nearly_equal('man', 'woman')
False
Defining solution of a problem in terms of the same problem, typically of smaller size, is called recursion.
Recursion makes it possible to express solution of a problem very concisely and elegantly.
def exp(x, n):
    """Computes the result of x raised to the power of n."""
    if n == 0:
        return 1
    else:
        return x * exp(x, n-1)
exp(2, 4)
+-- 2 * exp(2, 3)
|       +-- 2 * exp(2, 2)
|       |       +-- 2 * exp(2, 1)
|       |       |       +-- 2 * exp(2, 0)
|       |       |       |       +-- 1
|       |       |       +-- 2 * 1
|       |       |       +-- 2
|       |       +-- 2 * 2
|       |       +-- 4
|       +-- 2 * 4
|       +-- 8
+-- 2 * 8
+-- 16
Faster implementation of exponent function.
def fast_exp(x, n):
    if n == 0:
        return 1
    elif n % 2 == 0:
        return fast_exp(x*x, n/2))
    else:
        return x * fast_exp(x, n-1)
fast_exp(2, 10)
+-- fast_exp(4, 5) # 2 * 2
|   +-- 4 * fast_exp(4, 4)
|   |       +-- fast_exp(16, 2) # 4 * 4
|   |       |   +-- fast_exp(256, 1) # 16 * 16
|   |       |   |   +-- 256 * fast_exp(256, 0)
|   |       |   |             +-- 1
|   |       |   |   +-- 256 * 1
|   |       |   |   +-- 256
|   |       |   +-- 256
|   |       +-- 256
|   +-- 4 * 256
|   +-- 1024
+-- 1024
1024
The following function computes factorial of a number.
def fact(n):
    if n == 0:
        return 1
    else:
        return n * fact(n-1)
It looks quite similar to our exp function. Can you improve the execution
time of this program by writing a fast_fact function? If not, explain why?
The sequence F(n) of Fibonacci numbers is defined by the recurrence relation:
F(n) = F(n-1) + F(n-2)
F(0) = 0 
F(1) = 1
And here is the Python version:
def fib(n):
    if n == 0 or n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
                     fib(4)
             +---------+--------+
             |                  |
           fib(3)             fib(2)     
       +-----+-----+         +---+---+   
    fib(2)       fib(1)      |       |   
   +---+---+       |       fib(1)  fib(0)
   |       |       1         |       |
 fib(1)  fib(0)              1       1   
   |       |
   1       1
How many different ways can we make change of 50 using coins of denomination 50, 25, 10 and 5?
50
25 + 25
25 + 10 + 10 + 5
25 + 10 + 5 + 5 + 5
25 + 5 * 5
5 * 10
4 * 10 + 2 * 5
3 * 10 + 4 * 5
2 * 10 + 6 * 5
1 * 10 + 8 * 5
10 * 5
11 ways.
What will be the number if we also have coin of denomination 1? Can we write a program to compute that?
The number of ways to change amount A is equal to:
A using all but the largest coin, plusA - D using all kinds of coins, where D is the denomination of the largest kind of coin.def count_change(amount, coins):
    """Counts the number ways in which amount can be made using the provided coins.
    Assumes that the coins are sorted in desceding order.
    """
    if amount < 0:
        return 0
    elif amount == 0:
        return 1
    elif not coins:
        return 0
    else:
        return count_change(amount-coins[0], coins) + \
            count_change(amount, coins[1:])
Lets try some examples.
>>> count_change(50, [50, 25, 10, 5])
11
>>> count_change(50, [50, 25, 10, 5, 1])
50
>>> count_change(100, [50, 25, 10, 5, 1])
292
>>> count_change(200, [50, 25, 10, 5, 1])
2435
>>> count_change(200, [50, 25, 10, 5, 2, 1])
58030
Functions that take functions as arguments or return other functions.
Consider the earlier fibonacci example.
def fib(n):
    if n == 0 or n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
Can we write a higher-order function to trace the execution of this function?
Here is the first attempt.
def trace(f):
    def g(x):
        print f.__name__, x
        value = f(x)
        print 'return', repr(value)
        return value
    return g
fib = trace(fib)
print fib(3)
Output:
fib 3
fib 2
fib 1
return 1
fib 0
return 1
return 2
fib 1
return 1
return 3
3
Lets try to pretty print the call log.
level = 0
def trace(f):
    def g(*args):
        global level
        indent = '|   ' * level + '|-- '
        argstr = ", ".join([repr(a) for a in args])
        print indent + f.__name__ + argstr
        level += 1
        value = f(*args)
        level -= 1
        print indent + ' return ' + repr(value)
        return value
    return g
Lets try our fib example.
fib = trace(fib)
print fib(4)
Output:
>>> print fib(4)
|-- fib(4)
|   |-- fib(3)
|   |   |-- fib(2)
|   |   |   |-- fib(1)
|   |   |   |   |-- return 1
|   |   |   |-- fib(0)
|   |   |   |   |-- return 1
|   |   |   |-- return 2
|   |   |-- fib(1)
|   |   |   |-- return 1
|   |   |-- return 3
|   |-- fib(2)
|   |   |-- fib(1)
|   |   |   |-- return 1
|   |   |-- fib(0)
|   |   |   |-- return 1
|   |   |-- return 2
|   |-- return 5
5
This can be very useful debugging tool.
@trace
def square(x):
    return x*x
@trace
def sum_of_squares(x, y):
    return square(x) + square(y)
print sum_of_square(3, 4)
Output:
|-- sum_of_squares(3, 4)
|   |-- square(3)
|   |   |-- return 9
|   |-- square(4)
|   |   |-- return 16
|   |-- return 25
25
fib function? Doing this is very popular in functional programming world and it is called memoize.
def memoize(f):
    cache = {}
    def g(x):
        if x not in cache:
            cache[x] = f(x)
        return cache[x]
    return g
fib = trace(fib)
fib = memoize(fib)
print fib(4)
Output:
|-- fib(4)
|   |-- fib(3)
|   |   |-- fib(2)
|   |   |   |-- fib(1)
|   |   |   |   |-- return 1
|   |   |   |-- fib(0)
|   |   |   |   |-- return 1
|   |   |   |-- return 2
|   |   |-- return 3
|   |-- return 5
5
Notice that the redundant computation is gone now.
Write a function profile, which takes a function as argument and returns a new function, which behaves exactly similar to the given function, except that it prints the time consumed in executing it.
>>> fib = profile(fib)
>>> fib(20)
time taken: 0.1 sec
10946
Write a function vectorize which takes a function f and returns a new
  function, which takes a list as argument and calls f for every element and
  returns the result as a list.
>>> def square(x): return x * x
...
>>> f = vectorize(square)
>>> f([1, 2, 3])
[1, 4, 9]
>>> g = vectorize(len)
>>> g(["hello", "world"])
[5, 5]
Iterators are special objects that can be iterated through.
>>> x = iter([1, 2, 3])
>>> x
<listiterator object at 0x1004ca850>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Iterators can be implemented as classes. Here is an iterator that works like xrange.
class yrange:
    def __init__(self, n):
        self.i = 0
        self.n = n
    def __iter__(self):
        return self
    def next(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()
Lets see it working:
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 14, in next
StopIteration
Many built-in functions accept iterators as arguments.
>>> list(yrange(3))
[0, 1, 2]
Generators simplifies creation of iterators.
def yrange(n):
    i = 0
    while i < n:
        yield i
        i += 1
Lets try it out.
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 14, in next
StopIteration
>>> list(yrange(3))
[0, 1, 2]
def integers():
    i = 1
    while True:
        yield i
        i = i + 1
def squares():
    for i in integers():
        yield i * i
def take(n, seq):
    """Returns first n values from the given sequence."""
    seq = iter(seq)
    result = []
    try:
        for i in range(n):
            result.append(seq.next())
    except StopIteration:
        pass
    return result
Lets try it.
>>> take(5, squares())
[1, 4, 9, 16, 25]
Generator expressions take generators to the next level.
See improved versions of squares and take functions using generator expressions.
def integers():
    i = 1
    while True:
        yield i
        i = i + 1
def squares():
    return (i*i for i in integers())
def take(n, seq):
    seq = iter(seq)
    return list(seq.next() for i in range(n))
See it in action.
>>> take(5, squares())
[1, 4, 9, 16, 25]
| Table of Contents | t | 
|---|---|
| Exposé | ESC | 
| Full screen slides | e | 
| Presenter View | p | 
| Source Files | s | 
| Slide Numbers | n | 
| Toggle screen blanking | b | 
| Show/hide slide context | c | 
| Notes | 2 | 
| Help | h |