The content for this site available on GitHub. If you want to launch the notebooks interactively click on the binder stamp below. Binder

< Logging Errors and Exceptions in Python | Contents

Decorators in Python

Purpose: To help you get comfortable with the topics outlined below.

Recomended Usage

  • Run each of the cells (Shift+Enter) and edit them as necessary to solidify your understanding
  • Do any of the exercises that are relevant to helping you understand the material

Topics Covered

  • Decorators

Workbook Setup

The following magics will reload all modules before executing a new line and help make sure you follow PEP8 code style.

In [1]:
%load_ext autoreload
%autoreload 2

%load_ext pycodestyle_magic
%pycodestyle_on
In [7]:
import functools

Decorators

Decorators are wrappers that make code reuse easy. They are function/class wrappers that can be used to modify the input, output or even the function/class itself before execution.

@decorator

A bit more background on when and why decorators make sense to use

**Note:** Skip this section if you are comfortable with when/where/why you would use decorators.

If you are working with a codebase adding new features or getting it ready for production there is a good chance it will be useful to have a set of tools that profile how the code performs based on its application.

Maybe you will want to see how a particular function utilizes memory or the CPU. Maybe you have a set of custom debugging tasks that show you memory locations or other thing in a specifc way. Decorators allow you to do these things.

They allow you to just "decorate" the function/class with a little "@my_decorator" keyword above the function/class and viola! You can get a CPU profile, memory profile, memory locations of variables, show local and global vars and values, etc, etc. Whatever you write into the decorator you define.

As you can imaging, this can be a very powerful tool! Let's dive in!

Basic Decorators

Define my_decorator to take a function then defines an inner function that wraps that function

In [2]:
# Define a decorator
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

Now we can run a function with the decorator we just created

In [3]:
def say_hi():
    print("hi")


greeting = my_decorator(say_hi)
greeting()
Something is happening before the function is called.
hi
Something is happening after the function is called.

Instead of actually having to wrap the function inside our decorator like we did above

greeting = my_decorator(say_hi)

we can just use the @my_decorator notation to define a decorator (or wrapper) for a function.

In [6]:
# Use the decorator you defined to wrap a function
@my_decorator
def say_hi():
    print("hi")
    
say_hi()
Something is happening before the function is called.
hi
Something is happening after the function is called.

The big take home here is that even though all we did was call say_hi(), becuase it was decorated we were actually able to do extra stuff (in this case just print stuff before and after the function call but we'll do more interesting/useful stuff soon).

Decorators using functools

The above example of a decorator is fine if we don't pass any function arguments but as you can see below, it breaks if we try and pass an argument.

In [7]:
@my_decorator
def say_hi_2(name):
    print("hi {}".format(name))
In [8]:
say_hi_2("james")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-7b1c34028470> in <module>
----> 1 say_hi_2("james")

TypeError: wrapper() takes 0 positional arguments but 1 was given

To fix this, we (ie preserve all information) we need to use a special decorator called @functools.wraps

In [12]:
def my_decorator2(func):
    @functools.wraps(func)
    def wrapper_my_decorator(*args, **kwargs):
        print("Something is happening before the function is called.")
        value = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return value
    return wrapper_my_decorator
In [13]:
@my_decorator2
def say_hi_2(name):
    print("hi {}".format(name))
In [14]:
say_hi_2("james")
Something is happening before the function is called.
hi james
Something is happening after the function is called.

Great! Now lets actually do some useful stuff with decorators

In [ ]:

Timing using decorators

In [15]:
import time
In [16]:
def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer
In [20]:
@timer
def pass_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])
In [21]:
pass_time(1)
Finished 'pass_time' in 0.0046 secs
In [22]:
pass_time(999)
Finished 'pass_time' in 3.7255 secs

Debugging using decorators

In [8]:
def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")
        return value
    return wrapper_debug
In [9]:
@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"
In [10]:
make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
Out[10]:
'Whoa Richard! 112 already, you are growing up!'
In [26]:
make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
Out[26]:
'Whoa Dorrisile! 116 already, you are growing up!'

Very decorative functions

Note: A function can be very decorative (ie. have multiple decorators):

@decorator_1
@decorator_2
@decorator_3
def hello():
    print('hello')

Troubleshooting Tips

If you run into issues running any of the code in this notebook, check your version of Python and Jupyter against mine below

import sys
print(sys.version)
3.7.6 (default, Dec 31 2019, 17:12:14) 
[Clang 11.0.0 (clang-1100.0.33.16)]
!jupyter --version
jupyter core     : 4.6.1
jupyter-notebook : 6.0.2
qtconsole        : not installed
ipython          : 7.9.0
ipykernel        : 5.1.3
jupyter client   : 5.3.4
jupyter lab      : 1.2.3
nbconvert        : 5.6.1
ipywidgets       : not installed
nbformat         : 4.4.0
traitlets        : 4.3.3
In [7]:
# import sys
# print(sys.version)
In [6]:
# !jupyter --version


< Logging Errors and Exceptions in Python | Contents