Decorators and Functional Programming in Python

Derived from the notebook: http://anandology.com/nb/2014/decorators-demystified/ (Was a workshop in Pycon India '14)

In [1]:
def square(x):
    return x*x

print(square(4))
16
In [2]:
print(square)
<function square at 0x7f8fdd684ae8>
In [3]:
f = square
In [4]:
print(f)
<function square at 0x7f8fdd684ae8>

They're called first-class objects

In [5]:
def sum_of_square(x, y):
    return square(x) + square(y)
In [6]:
sum_of_square(3,4)
Out[6]:
25
In [7]:
def sum_of(f, x, y):
    return f(x) + f(y)

sum_of(square, 2, 3)
Out[7]:
13
In [8]:
def mod3(x):
    return x % 3

sum_of(mod3, 4, 5)
Out[8]:
3
In [9]:
sum_of(lambda x: x %3, 4, 5)
Out[9]:
3
In [10]:
mod4 = lambda x: x % 4
In [11]:
mod4(17)
Out[11]:
1
In [12]:
print(lambda x: x % 4)
<function <lambda> at 0x7f8fdce3a158>
In [13]:
max(["Python", "Haskell"], key=len)
Out[13]:
'Haskell'
In [14]:
print(len)
<built-in function len>
In [15]:
len("Python")
print("python" > "haskell")
print(len("python") > len("haskell"))
True
False
In [16]:
def incr(x, amount=1):
    return x + amount
In [17]:
incr(4)
Out[17]:
5
In [18]:
incr(4, 2)
Out[18]:
6
In [19]:
def sub(x, y):
    return x-y
In [20]:
sub(3, 5)
Out[20]:
-2
In [21]:
sub(x=3, y=5)
Out[21]:
-2
In [22]:
sub(y=5, x=3)
Out[22]:
-2
In [23]:
max(1,2,3,4,5)
Out[23]:
5
In [24]:
def f(*args):
    print(args)
    
f(2,3,5)
f(8,1)
(2, 3, 5)
(8, 1)
In [25]:
def xprint(label, *args):
    print(type(args))
    for a in args:
        print(label, a)
In [26]:
xprint("Info", 1,2,3)
<class 'tuple'>
Info 1
Info 2
Info 3

Problem Implement a function maximum that takes 2 values x and y and a key function as argument and finds the maximum by comparing key(x) and key(y).

>>> maximum(3, -4, abs)
-4
>>> maximum("Python", "Haskell", len)
'Haskell'
>>> maximum("java", "Python", lambda s: s.lower())
'Python'



Problem Write a function strjoin that takes a separator as first argument followed by variable number of strings to join with that separator.

>>> strjoin("-", "a", "b", "c")
"a-b-c"
In [27]:
def maximum(*args, key=lambda x: x):
    best = args[0]
    for i in args:
        if (key(i) > key(best)):
            best = i
    return best
In [28]:
maximum('aloo', 'python', 'z', 'Haskell', key=len)
Out[28]:
'Haskell'
In [29]:
def strjoin(sep, *args):
    return sep.join(args)

strjoin("-", "a", "b", "c")
Out[29]:
'a-b-c'
In [30]:
# Just like variable args, you can have variable keword args too, just use ** instead of *

def f(**kwargs):
    print(kwargs)
In [31]:
f(x=1, y=2, abcd=3)
{'abcd': 3, 'y': 2, 'x': 1}
In [32]:
[int(x) for x in '2 3 4 5'.split()] # List comprehension
Out[32]:
[2, 3, 4, 5]
In [33]:
[(k,v) for k, v in {'title': 'IIIT-D', 'href': 'http://iiitd.ac.in'}.items()]
Out[33]:
[('href', 'http://iiitd.ac.in'), ('title', 'IIIT-D')]
In [34]:
"Hey %s" % ('jai')
Out[34]:
'Hey jai'

Problem Make a function render_tag(tagname, **attrs) that returns a html tag as a string

>>> render_tag("a", href="http://iiitd.ac.in", title="IIIT-D")
'<a href="http://iiitd.ac.in", title="IIIT-D">'
In [35]:
def render_tag(tagname, **attrs):
    pairs = ['%s="%s"' % (k,v) for k, v in attrs.items()]
    pairs_str = " ".join(pairs)
    return "<%s %s>" % (tagname, pairs_str)
In [36]:
render_tag("a", href="http://iiitd.ac.in", title="IIIT-D")
Out[36]:
'<a href="http://iiitd.ac.in" title="IIIT-D">'
In [37]:
# Currying
def make_adder(x):
    def add(y):
        return x + y
    return add
In [38]:
add5 = make_adder(5)
add5(2)
Out[38]:
7
In [39]:
add6 = make_adder(6)
add6(7)
Out[39]:
13
In [40]:
make_adder(8)(5)
Out[40]:
13

Decorators

In [41]:
%%file trace0.py

def trace(f):
    def wrapper(*args):
        print(f.__name__, args)
        return f(*args)
    return wrapper
Overwriting trace0.py
In [42]:
%%file sum.py

from trace0 import trace

@trace
def square(x):
    # print("square", x)a
    return x*x

#square = trace(square)

@trace
def sum_of_squares(x, y):
    # print("sum_of_squares", x, y)
    return square(x) + square(y)

#sum_of_squares = trace(sum_of_squares)

print(sum_of_squares(3,4))
Overwriting sum.py
In [43]:
!python sum.py
sum_of_squares (3, 4)
square (3,)
square (4,)
25
In [44]:
%%file trace1.py

level = 0

def trace(f):
    def wrapper(*args):
        global level
        print("|  " * level + "|--", f.__name__, args)
        
        level += 1
        result = f(*args)
        level -= 1
        
        return result
    return wrapper
Overwriting trace1.py
In [45]:
%%file fibo.py

from trace1 import trace
from memoize import memoize

@trace
def fib(n):
    if n==0 or n==1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
print(fib(7))
Overwriting fibo.py
In [46]:
!python fibo.py
|-- fib (7,)
|  |-- fib (6,)
|  |  |-- fib (5,)
|  |  |  |-- fib (4,)
|  |  |  |  |-- fib (3,)
|  |  |  |  |  |-- fib (2,)
|  |  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |  |-- fib (0,)
|  |  |  |  |  |-- fib (1,)
|  |  |  |  |-- fib (2,)
|  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |-- fib (0,)
|  |  |  |-- fib (3,)
|  |  |  |  |-- fib (2,)
|  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |-- fib (0,)
|  |  |  |  |-- fib (1,)
|  |  |-- fib (4,)
|  |  |  |-- fib (3,)
|  |  |  |  |-- fib (2,)
|  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |-- fib (0,)
|  |  |  |  |-- fib (1,)
|  |  |  |-- fib (2,)
|  |  |  |  |-- fib (1,)
|  |  |  |  |-- fib (0,)
|  |-- fib (5,)
|  |  |-- fib (4,)
|  |  |  |-- fib (3,)
|  |  |  |  |-- fib (2,)
|  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |-- fib (0,)
|  |  |  |  |-- fib (1,)
|  |  |  |-- fib (2,)
|  |  |  |  |-- fib (1,)
|  |  |  |  |-- fib (0,)
|  |  |-- fib (3,)
|  |  |  |-- fib (2,)
|  |  |  |  |-- fib (1,)
|  |  |  |  |-- fib (0,)
|  |  |  |-- fib (1,)
21
In [47]:
%%file memoize.py

def memoize(f):
    
    cache = {}
    
    def wrapper(*args):
        if args not in cache:
            cache[args] = f(*args)
        return cache[args]
    return wrapper
Overwriting memoize.py
In [48]:
%%file fibo1.py

from trace1 import trace
from memoize import memoize

@memoize
@trace
def fib(n):
    if n==0 or n==1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
print(fib(7))
Overwriting fibo1.py
In [49]:
!python fibo1.py
|-- fib (7,)
|  |-- fib (6,)
|  |  |-- fib (5,)
|  |  |  |-- fib (4,)
|  |  |  |  |-- fib (3,)
|  |  |  |  |  |-- fib (2,)
|  |  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |  |-- fib (0,)
21

Problem Write a function with_retries that continue to retry for 5 times if there is any exception raised in the function.

@with_retries
def wget(url):
    return urllib2.urlopen(url).read()

wget("http://google.com/no-such-page")

Should print:

Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Giving up!
In [50]:
%%file with_retries.py

import urllib.request

def with_retries(f, num):
    def wrapper(*args):
        for i in range(num):
            try:
                return f(*args)
            except:
                print("Failed")
        print("Given up!")
    return wrapper

#@with_retries
def wget(url):
    return urllib.request.urlopen(url)

wget = with_retries(wget, 7)
x = wget("http://google.com/pageisnonexistent")
Overwriting with_retries.py
In [51]:
!python with_retries.py
Failed
Failed
Failed
Failed
Failed
Failed
Failed
Given up!
In [52]:
%%file with_retries_adv.py

def with_retries(num):
    def decor(f):
        def wrapper(*args):
            for i in range(num):
                try:
                    return f(*args)
                except:
                    print("failed")
            print("given up!")
        return wrapper
    return decor

@with_retries(3)
def wget(url):
    return urllib.request.urlopen(url)

# wget = with_retries(3)(wget)

x = wget("http://google.com/pageisnonexistent")
Overwriting with_retries_adv.py
In [53]:
!python with_retries_adv.py
failed
failed
failed
given up!

Web framework - JUG (nano flask)

In [54]:
%%file jug.py

mapping = []

def route(path):
    def decor(f):
        mapping.append((path, f))
    return decor

def request(path):
    for p, func in mapping:
        if p == path:
            return func()
    return "404"

def wsgifunc(env, start_response):
    path = env['PATH_INFO']
    start_response('200 OK', [("Content-Type", "text/plain")])
    return [request(path).encode('utf-8')]

def run(port=8080):
    from wsgiref.simple_server import make_server
    server = make_server("0.0.0.0", port, wsgifunc)
    server.serve_forever()
Overwriting jug.py
In [55]:
%%file hello.py

from jug import route

@route('/hello')
def hello():
    return "Hello! :D"

@route('/bye')
def bye():
    return "Good bye!"
Overwriting hello.py
In [56]:
%%file client.py
import hello
from jug import request, run
import sys

if "--web" in sys.argv:
    run()
else:
    print(request("/hello"))
    print(request("/bye"))
Overwriting client.py
In [57]:
!python client.py
Hello! :D
Good bye!
In [59]:
!python client.py --web 
127.0.0.1 - - [28/Oct/2015 21:49:49] "GET /hello HTTP/1.1" 200 9
^CTraceback (most recent call last):
  File "client.py", line 6, in <module>
    run()
  File "/home/darkapex/decorators/jug.py", line 23, in run
    server.serve_forever()
  File "/usr/lib/python3.5/socketserver.py", line 237, in serve_forever
    ready = selector.select(poll_interval)
  File "/usr/lib/python3.5/selectors.py", line 367, in select
    fd_event_list = self._poll.poll(timeout)
KeyboardInterrupt