Decorators#

%pylab inline
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib

We’ve seen Python functions and objects. Today, we’ll breifly cover decorators, which are functions that wrap functions, and come with sytactic sugar which lets you write things like

@time_wrapper
def my_function(x):
    ...
    
y = my_function(x)
> 1.1 seconds elapsed.

Again, we want to write a function that wraps another function. We can define this as

from time import monotonic

def time_wrapper(f):
    
    def inner_wrapper(*args, **kwargs):
        t0 = monotonic()
        ret = f(*args, **kwargs)
        t1 = monotonic()
        print("{} sec. elapsed".format(t1 - t0))
        return ret
    
    return inner_wrapper

Here’s how we might use our wrapper:

def generate_random(n):
    """
    return a random numpy vector
    """
    return np.random.rand(n)

generate_random = time_wrapper(generate_random)

x = generate_random(100000)
len(x)
0.001911900999999716 sec. elapsed
100000

one problem with this is that we had to go through the trouble of wrapping the function. Instead, we can just write

@time_wrapper
def generate_random2(n):
    """
    return a random numpy vector
    """
    return np.random.rand(n)

generate_random2(100000)
0.0010340000000041982 sec. elapsed
array([0.22096675, 0.90420534, 0.39143721, ..., 0.66973555, 0.76240249,
       0.08790074])

This is interpreted like what we did above, but is a bit easier to read.

Another problem we may encounter is that we now can’t access the docstring we wrote

help(generate_random2)
Help on function inner_wrapper in module __main__:

inner_wrapper(*args, **kwargs)

One way to solve this is to use the wraps decorator from the functools package.

from functools import wraps

def time_wrapper2(f):
    
    @wraps(f)
    def inner_wrapper(*args, **kwargs):
        t0 = monotonic()
        ret = f(*args, **kwargs)
        t1 = monotonic()
        print("{} sec. elapsed".format(t1 - t0))
        return ret
    
    return inner_wrapper

@time_wrapper2
def generate_random3(n):
    """
    return a random numpy vector
    """
    return np.random.rand(n)

generate_random3(100000)
0.0012848000000076354 sec. elapsed
array([0.1303301 , 0.90833579, 0.48694508, ..., 0.4319377 , 0.68432426,
       0.31666559])

now, we can access the metadata from generate_random3, such as the docstring

help(generate_random3)
Help on function generate_random3 in module __main__:

generate_random3(n)
    return a random numpy vector

As you might guess, the @wraps decorator copies the class metadata from f to the inner_wrapper function.