Decorators

Decorators are a way to do aspect oriented programming in Python. Decorators were added in Python 2.4 and many libraries make extensive use of them.

In the simplest form

@dec
def foo():
    pass

is equivalent to

def foo():
    pass
foo = dec(foo)

For the above you can see that a decorator is a function that takes a function as an argument and returns a function.

Notes

Since their introduction in Python 2.4, decorators are everywhere. Including some in the standard library: property, staticmethod and others. Many libraries also use decorators: Celery @task, Flask @app.route and many more.

Decorators can be nested

@logged
@metrics.ncalls
@retry(times=3)
def whos_on_first():
    return 'who'


There are also class decorators (added in Python 2.6). You will probably won't need them (unless you wrote metaclasses before).

@plugin
class FunnyPlugin(object):
    pass

Exercise: Greeter

Create a decorator that prints 'Hello FUNCTION_NAME' before the function start and 'By FUNCTIONNAME' after the function finishes. (You can can the function name from the `__name__` attribute).


In [19]:
from functools import wraps
def greeter(fn):
    @wraps(fn)
    def wrapper(*args, **kw):
        print('Hello {}'.format(fn.__name__))
        try:
            return fn(*args, **kw)
        finally:
            print('Bye {}'.format(fn.__name__))
    
    return wrapper

In [21]:
@greeter
def add(x, y):
    '''Adds x to y'''
    return x / y

print(add(1, 2))
print(add.__name__)


Hello add
Bye add
0
add

Exercise: Timer

Write a timed decorators that logs timing information for a given function.


In [24]:
from time import time
def timed(fn):
    @wraps(fn)
    def wrapper(*args, **kw):
        start = time()
        try:
            return fn(*args, **kw)
        finally:
            duration = time() - start
            print('{} took ({:.2f}sec)'.format(fn.__name__, duration))

    return wrapper

In [25]:
from time import sleep
@timed
def mul(x, y):
    '''Multiply x with y'''
    sleep(0.2)
    return x * y

mul(8, 4)
# return 32 and print timing info


mul took (0.20sec)
Out[25]:
32

Bonus: Use timed_block

Rewrite your decorator using timed_block context manager.


In [ ]:
# Your code goes here

Exercise: Caching

Write a decorator that caches the results of the invoked function. Computing every value only once. (See lru_cache in Python > 3.2).


In [4]:
def cached(fn):
    cache = {}  # Can be accessed from wrapper

    def wrapper(*args, **kw):
        # You code goes here
        # Remember that dict keys can be any immutable object and `args` is a tuple)
        # For simplicity, you can ignore **kw
        pass
        
    return wrapper

In [35]:
@cached
def fib(n):
    '''Return the n'th fibonacci number'''
    print('fib({})'.format(n))
    if n < 2:
        return 1
    return fib(n-1) + fib(n-2)

fib(5)


fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
Out[35]:
8

Exercise: Multiplier

Write a decorator the multiplies the result of its wrapped function by n, where n is a parameter to the decorator.


In [ ]:
# Your code goes here

In [ ]:
@mulby(7)
def inc(x):
    '''Add 1 to x'''
    return x + 1

inc(2)  # 21

In [38]:
class A(object):
    def __init__(self, x):
        self._x = x
        
    @property
    def x(self):
        return self._x
    
    @staticmethod
    def y():
        return 8
    
a = A(7)
print(a.x)
print(A.y())


7
8

@property decorator

property makes a function looks like an ordinary attribute. It let's you change your mind and have more controlled access to attribute (say locking) without any change to the client.


In [5]:
class Parrot(object):
    def __init__(self, voltage):
        self._voltage = voltage
        
    @property
    def voltage(self):
        return self._voltage
    
p = Parrot(10)
print(p.voltage)  # Note: Not a function call


10

In [ ]: