Understanding Decorators

Python Decorators in 12 Steps


In [2]:
# 1. Functions
def foo():
    return 1
foo()


Out[2]:
1

In [14]:
# 2. Scope
a_string = "This is global"
def foo_scope():
    a_string = "This is local"
    print locals()
foo_scope()
print globals()['a_string']


{'a_string': 'This is local'}
This is global

In [23]:
# 3. Variable resolution rules
def foo_vrr():
    # variable scope is resolved by first checking the local scope
    # and if it isn't found, it is considered a new var, even if
    # it uses the same namespace
    a_string = "test"
    print locals()
foo_vrr()
a_string


{'a_string': 'test'}
Out[23]:
'This is global'

In [30]:
# 4. Variable Lifetime
def foo_vl():
    x = 1
foo_vl()
# print x gives an error

In [33]:
# 5. Function arguments and parameters
# functions are first-class objects, so they can be passed as params to a func
def foo_fap(x):
    print locals()

foo_fap(1)


{'x': 1}

In [42]:
# 6. Nested functions
def outer():
    x = 1
    def inner():
        print x
    inner()
# outer()
thing = outer
# thing()

In [53]:
# 7. Funcs are 1st Class
print issubclass(int, object)
print isinstance("hello", object)

# all objects in python are subclass of object
def foo_fcc():
    print 'hello'
    
print foo_fcc.__class__
print type(foo_fcc)
print issubclass(foo_fcc.__class__, object)


True
True
<type 'function'>
<type 'function'>
True

In [57]:
# functions that receive functions as parameters
def add(x, y):
    return x + y
def sub(x, y):
    return x - y
def apply_operation(func, x, y):
    return func(x, y)

print apply_operation(add, 2, 1)
print apply_operation(sub, 2, 5)


3
-3

In [67]:
# functions can also return functions
def outer():
    def inner():
        print "Inside inner"
    # now, we just pass back a function label
    return inner

# but by doing this we can assign the inner function to another variable
foo = outer()
print foo
foo()

def outer_one():
    def inner(x):
        print "Inside with {0}".format(x)
    return inner
bar = outer_one()
print bar
bar("mittens")


<function inner at 0x1062e8140>
Inside inner
<function inner at 0x1062e8230>
Inside with mittens

In [70]:
# 8. Closures
def outer():
    x = 1
    def inner():
        print x
    return inner

foo = outer()
print foo
print foo.func_closure


<function inner at 0x1062e80c8>
(<cell at 0x105fb1600: int object at 0x7fb08f603b88>,)

In [72]:
# closures: functions remember their enclosing scope at the time of definition
def outer_param(x):
    def inner():
        print x
    return inner

print1 = outer_param(1)
print2 = outer_param(2)
print1()
print2()


1
2

In [82]:
# 9. Decorators! 
# a decorator is just a callable that takes a function as an argument 
# and returns a replacement function

def outer(some_func):
    def innert():
        print "before some_func"
        ret = some_func() # get the value of some_func, store it
        return ret + 1 # return the value + 1
    return innert # return the innert func label

def return_one():
    return 1

decorated = outer(return_one) 
print decorated()
# you could say: decorated is a decorated version of return_one, because
# it does return_one plus something else

# we could also replace the original version of return_one with the 
# decorated form so that we would always get our "plus something else" 
# version of return_one
return_one = outer(return_one)
print return_one()

# now, any future calls to return_one will return the decorated version
# instead of the original
print return_one()


before some_func
2
before some_func
2
before some_func
2

In [83]:
class Coordinate(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return "Coord: " + str(self.__dict__)
    
def add(a, b):
    return Coordinate(a.x + b.x, a.y + b.y)
def sub(a, b):
    return Coordinate(a.x - b.x, a.y - b.y)

one = Coordinate(100,200)
two = Coordinate(300,200)
add(one, two)


Out[83]:
Coord: {'y': 400, 'x': 400}

In [85]:
# the above works well, but what if we had this:
three = Coordinate(-100, -100)
print sub(one, two)
print add(one, three)

# we'd rather have the difference of one and two be {0, 0} and the sum of 
# one and three be {100, 200} without having to modify one, two or three


Coord: {'y': 0, 'x': -200}
Coord: {'y': 100, 'x': 0}

In [87]:
# one solution would be to add a bounds checking decorator
def wrapper(func):
    def checker(a, b):
        if a.x < 0 or a.y < 0:
            a = Coordinate(a.x if a.x>0 else 0, a.y if a.y>0 else 0)
        if b.x < 0 or b.y < 0:
            b = Coordinate(b.x if b.x>0 else 0, b.y if b.y>0 else 0)
        ret = func(a,b)
        if ret.x < 0 or ret.y < 0:
            ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y>0 else 0)
        return ret
    return checker

add = wrapper(add)
sub = wrapper(sub)

# now, the functions add and sub work as before, but with a little
# something extra -- hence they are 'decorated'

In [88]:
sub(one, two)


Out[88]:
Coord: {'y': 0, 'x': 0}

In [89]:
add(one, three)


Out[89]:
Coord: {'y': 200, 'x': 100}

In [97]:
# 10. The @ symbol applies a decorator to a function
# prior to python 2.4, you'd have to rename the function label to have the
# decorated effect. but now, you can explicitly declare it

# this:
add = wrapper(add)

# is the same as:
@wrapper
def add(a,b):
    print 'much decorated'
    return Coordinate(a.x+b.x, a.y+b.y)

add(one, three)


much decorated
Out[97]:
Coord: {'y': 200, 'x': 100}

In [96]:
@wrapper
def sub(a,b):
    print 'param decorated'
    return Coordinate(a.x-b.x, a.y-b.y)
sub(one, two)


param decorated
Out[96]:
Coord: {'y': 0, 'x': 0}

In [101]:
# 11. *args and **kwargs
# the * operator when used in defining a function means that
# any extra positional arguments passed to the function end up
# with the variable prefaced with a *. So:
def one(*args):
    print args
    
one()
one(1, 2, 3)

def two(x,y,*args):
    print x, y, args
    
two('a','b','c')
two(1,2,3,4,5,6,7,8,9)


()
(1, 2, 3)
a b ('c',)
1 2 (3, 4, 5, 6, 7, 8, 9)

In [106]:
'''
The * operator, when prefacing a variable that is passed into a function
means that the variable contents should be extracted and used a positional
arguments
'''
def add(x, y):
    return x + y
lst = [1, 2]
# the below two function the same
print add(lst[0], lst[1])
print add(*lst)


3
3

In [108]:
# ** does for dicts and k/v pairs what * does for positional parameters
def foobar(**kwargs):
    print kwargs
foobar()
foobar(x=1, y=2)


{}
{'y': 2, 'x': 1}

In [112]:
# neither args nor kwargs are part of the python language, it is just
# convention to use those words
def barfoo(x, y):
    print x+y
dct = {'x':1, 'y':2} # these must match the expected param labels, x/y
barfoo(**dct)


3

In [118]:
# 12. More generic decorators
'''
We can now write a decorator that "logs" the arguments to a functions:
'''
def logger(func):
    def inner(*args, **kwargs):
        print 'Arguments were: %s, %s' % (args, kwargs)
        return func(*args, **kwargs)
    return inner

'''
The inner function takes any arbitrary number of type of parameters and
passes them along as arguments to the wrapped function, inner. This allows
you to wrap or decorate any function, no matter the signature
'''

# mitten = logger(mitten)
@logger
def mitten(x, y=5):
    return x*y

# smitten = logger(smitten)
@logger
def smitten():
    return 2

# calling these functions results in a "logging" output line, as well
# as the expected return value of each function
print mitten(5, 4)
print mitten(10)
print smitten()


Arguments were: (5, 4), {}
20
Arguments were: (10,), {}
50
Arguments were: (), {}
2

In [ ]: