# Decorators Demystified

Anand Chitipotu
Python India 2014 - Sept 26, 2014

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

print square(4)
16

In [3]:
print square
<function square at 0x10b3ccaa0>

In [4]:
f = square
In [5]:
print f
<function square at 0x10b3ccaa0>

In [6]:
print f(4)
16

In [7]:
#square = <new-function-here>
In [8]:
def sum_of_squares(x, y):
return square(x) + square(y)
In [9]:
sum_of_squares(3, 4)
Out[9]:
25
In [10]:
def cube(x):
return x*x*x

def sum_of_cubes(x, y):
return cube(x) + cube(y)

print sum_of_cubes(3, 4)
91

In [11]:
def sum_of(f, x, y):
return f(x) + f(y)

print sum_of(square, 3, 4)
print sum_of(cube, 3, 4)
print sum_of(abs, 3, -4)
25
91
7

In [12]:
def mod3(x):
return x % 3

print sum_of(mod3, 4, 8)
3

In [13]:
print sum_of(lambda x: x%3, 4, 8)
3

In [14]:
print sum_of(lambda x: x*x*x, 4, 8)
576

In [15]:
max(3, 4)
Out[15]:
4
In [16]:
max(["Python", "Java"])
Out[16]:
'Python'
In [18]:
max(["Python", "Haskell"])
Out[18]:
'Python'
In [19]:
max(["Python", "Haskell"], key=len)
Out[19]:
'Haskell'
In [22]:
names = ["C", "Java", "C++", "Perl", "Python", "Ruby", "Haskell"]
sorted(names)
Out[22]:
['C', 'C++', 'Haskell', 'Java', 'Perl', 'Python', 'Ruby']
In [23]:
sorted(names, key=len)
Out[23]:
['C', 'C++', 'Java', 'Perl', 'Ruby', 'Python', 'Haskell']
In [24]:
len("hello")
Out[24]:
5

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'
In [27]:
max("java", "Python",)
Out[27]:
'java'
In [28]:
max("java", "Python", key=lambda s: s.lower())
Out[28]:
'Python'
In [29]:
def maximum(x, y, key):
if key(x) > key(y):
return x
else:
return y
In [30]:
print maximum(3, -4, abs)
-4

In [31]:
maximum("java", "Python", lambda s: s.lower())
Out[31]:
'Python'

## Default Arguments¶

In [32]:
def incr(x, amount=1):
return x+amount

print incr(4)
5

In [33]:
print incr(4, amount=2)
print incr(4, 2)
6
6

In [34]:
def sub(x, y):
return x-y

print sub(3, 2)
1

In [36]:
print sub(x=3, y=2)
1

In [37]:
print sub(y=2, x=3)
1

In [39]:
print sub(3, y=2)
1

## Variable number of arguments and keyword arguments¶

In [40]:
max(1, 2, 3)
Out[40]:
3
In [41]:
max(1, 2, 3, 4)
Out[41]:
4
In [43]:
def f(*a):
print a

f()
f(1)
f(1, 2)
f(1, 2, 3)
()
(1,)
(1, 2)
(1, 2, 3)

In [44]:
def xprint(label, *args):
for a in args:
print label, a

xprint("INFO", 1, 2, 3)
INFO 1
INFO 2
INFO 3

Problem Implement a function add that takes variable number of arguments and returns their sum.

Hint: You can use built-in function sum for computing sum of a list of numbers.

>>> add(1, 2, 3)
6
>>> add(1, 2, 3, 4)
10

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"

Just like variable arguments, we can write functions that can take arbitrary keyword arguments.

In [45]:
def f(**kwargs):
print kwargs
In [46]:
f(x=1, y=2)
{'y': 2, 'x': 1}

In [54]:
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)

print render_tag("a",
href="http://in.pycon.org/",
title="PyCon India 2014")
<a href="http://in.pycon.org/" title="PyCon India 2014">

In [55]:
def f(x, y):
return x+y
In [56]:
def call_func(f, args):
return f(*args)

print call_func(square, [3])
print call_func(sum_of_squares, [3, 4])
9
25

In [60]:
def call_func1(f, *args):
return f(*args)

print call_func1(square, 3)
print call_func1(sum_of_squares, 3, 4)
9
25

In [61]:
def call_func1(f, *args, **kwargs):
return f(*args, **kwargs)

print call_func1(square, 3)
print call_func1(sum_of_squares, 3, 4)
print call_func1(square, x=3)
print call_func1(sum_of_squares, x=3, y=4)
9
25
9
25

In [62]:
print call_func1(sum_of_squares, 3, y=4)
25

## Functions as return value¶

In [63]:
def make_adder(x):
def add(y):
return x+y
return add

add5 = make_adder(5)
print add5(2)
7

In [64]:
data = [["A", 10], ["B", 34], ["C", 5]]
print max(data)
['C', 5]

In [69]:
def column(n):
def f(row):
return row[n]
return f

print max(data, key=column(1))
['B', 34]

## Decorators¶

In [76]:
%%file sum.py

def square(x):
print "square", x
return x*x

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

if __name__ == "__main__":
print sum_of_squares(3, 4)
Overwriting sum.py

In [77]:
!python sum.py
sum_of_squares 3 4
square 3
square 4
25

In [109]:
%%file trace0.py

def trace(f):
def g(*args):
print f.__name__, args
return f(*args)
return g
Overwriting trace0.py

In [105]:
%%file sum1.py

from trace0 import trace

@trace
def square(x):
return x*x

# @trace is same as:
# square = trace(square)

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

if __name__ == "__main__":
print sum_of_squares(3, 4)
print square
Overwriting sum1.py

In [106]:
!python sum1.py
sum_of_squares (3, 4)
square (3,)
square (4,)
25
<function g at 0x102aecd70>

In [101]:
%%file blackhole.py

def blackhole(f):
return 0

@blackhole
def square(x):
return x*x

print square
Writing blackhole.py

In [102]:
!python blackhole.py
0

In [122]:
%%file trace1.py
import functools
import os

level = 0
def trace(f):
if os.getenv("DEBUG") != "true":
return f

@functools.wraps(f)
def g(*args):
global level
print "|  " * level + "|--", f.__name__, args

level += 1
result = f(*args)
level -= 1
return result

#functools.update_wrapper(f, g)
return g
Overwriting trace1.py

In [125]:
%%file sum2.py

from trace1 import trace

@trace
def square(x):
return x*x

# @trace is same as:
# square = trace(square)

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

if __name__ == "__main__":
print sum_of_squares(3, 4)
Overwriting sum2.py

In [126]:
!python sum2.py
25

In [127]:
!DEBUG=true python sum2.py
|-- sum_of_squares (3, 4)
|  |-- square (3,)
|  |-- square (4,)
25

In [133]:
%%file fib0.py
from trace1 import trace

@trace
def fib(n):
if n == 0 or n == 1:
return 1
else:
return fib(n-1) + fib(n-2)

if __name__ == "__main__":
import sys
n = int(sys.argv[1])
print fib(n)
Overwriting fib0.py

In [137]:
!DEBUG=true python fib0.py 4
|-- fib (4,)
|  |-- fib (3,)
|  |  |-- fib (2,)
|  |  |  |-- fib (1,)
|  |  |  |-- fib (0,)
|  |  |-- fib (1,)
|  |-- fib (2,)
|  |  |-- fib (1,)
|  |  |-- fib (0,)
5

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 [145]:
%%file with_retries.py

import urllib2
import functools

def with_retries(f):
@functools.wraps(f)
def g(*args):
for i in range(5):
try:
return f(*args)
except:
print "Failed to download, retrying..."
print "Giving up!"
return g

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

x = wget("http://google.com/no-such-page")
Overwriting with_retries.py

In [146]:
!python with_retries.py
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Giving up!

In [156]:
%%file memoize.py

def memoize(f):
# create a cache for remembering return values
cache = {}

def g(*args):
# if the function is not called before with those arguments
if args not in cache:
# call it now and remember the result.
cache[args] = f(*args)
# return the remembered result
return cache[args]
return g
Overwriting memoize.py

In [157]:
%%file fib1.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)

if __name__ == "__main__":
import sys
n = int(sys.argv[1])
print fib(n)
Overwriting fib1.py

In [150]:
!DEBUG=true python fib1.py 10
|-- fib (10,)
|  |-- fib (9,)
|  |  |-- fib (8,)
|  |  |  |-- fib (7,)
|  |  |  |  |-- fib (6,)
|  |  |  |  |  |-- fib (5,)
|  |  |  |  |  |  |-- fib (4,)
|  |  |  |  |  |  |  |-- fib (3,)
|  |  |  |  |  |  |  |  |-- fib (2,)
|  |  |  |  |  |  |  |  |  |-- fib (1,)
|  |  |  |  |  |  |  |  |  |-- fib (0,)
89

In [155]:
!DEBUG=true python fib0.py 6
|-- 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,)
13

Now lets try to make the with_retries function take the number of retries as argument.

In [164]:
%%file with_retries1.py

import urllib2
import functools

def with_retries(num_retries):
# with_retries is not a decorator.
# the return value of with_retries is a decorator.
def decor(f):
@functools.wraps(f)
def g(*args):
for i in range(num_retries):
try:
return f(*args)
except:
print "Failed to download, retrying..."
print "Giving up!"
return g
return decor

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

#decor = with_retries(3)
#wget = decor(wget)

x = wget("http://google.com/no-such-page")
Overwriting with_retries1.py

In [165]:
!python with_retries1.py
Failed to download, retrying...
Failed to download, retrying...
Failed to download, retrying...
Giving up!

## Example: A web framework¶

In [189]:
%%file fakeweb.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 "not found"

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

def run(port=8080):
from wsgiref.simple_server import make_server
server = make_server("localhost", port, wsgifunc)
server.serve_forever()
Overwriting fakeweb.py

In [191]:
%%file hello.py
from fakeweb import route

@route("/hello")
def hello():
return "Hello, world!"

@route("/bye")
def bye():
return "Good bye!"

# @after_request
# def layout(response):
#     line = "\n" + "=" * 10 + "\n"
#     return line + response + line
Overwriting hello.py

In [187]:
%%file client.py
import hello
from fakeweb import request, run
import sys

if __name__ == "__main__":
if "--web" in sys.argv:
run()
else:
print request("/hello")
print request("/bye")
Overwriting client.py

In [190]:
!python client.py
Hello, world!
Good bye!

In []: