Get Fluent in Python Decorators by Visualizing It

Photo by Huyen Bui on Unsplash
Photo by Huyen Bui on Unsplash

Python decorator is syntactic sugar. You can achieve everything without explicitly using the decorator. However, Using the decorator can help your code be more concise and readable. Ultimately, you write fewer lines of code by leveraging Python decorators. 

Nevertheless, Python decorator isn’t a trivial concept to comprehend. Understanding Python decorators requires building blocks, including closure, function as an object, and deep knowledge of how Python code is executed. 

Many online resources talk about Python decorators, but many tutorials only provide some sample code walkthroughs. Reading sample code could help to get a basic understanding of Python decorator. When it comes time to implement your decorator, we might still need clarification and clarity about the decorator concept and yet have to refer to the online resource to refresh our memory for detail. 

Reading code sometimes doesn’t deepen memory but seeing images does. In this article, I want to help you understand Python decorator with some visualization and fun examples.

Python Functions are Objects

Python Functions are Objects | Image by Author
Python Functions are Objects | Image by Author

If we went to Python programming 101 class, we might get a drawing like this. We defined a variable, a representational name used to reference an object. The variable foo points to an object at a given time. It can be a list [1,2,3,4] , it can be a dictionary [“city”: “New York”], or it can a string “I like dumplings”

A less covered discussed topic in Python is that the variable foo can also point to a function add() . When the variable refers to a function, the foo can pass around the same way you used other types like int, str, list, or dict. 

What does that mean I can pass foo around? This unlocks us to pass a function as an argument. Furthermore, we can return a function as a return type

				
					# Python Functions are Objects
def I_am_1():
    return 1
def return_1():
    return I_am_1
def add(a, b):
    return a + b
foo = add
del add
## let remove the original defintion
print(foo(return_1()(),2))
## ouput 3
print(foo(foo(return_1()(),2),3))
## output 6
				
			

Code walkthrough

  • Function Definition: we defined three functions called add which adds two objects; I_am_1() which simply returns number 1; return_1 which return the function I_am_1
  • Interesting Fact: Then we point to another variable foo to it. To prove that function is an object like the other types, we can even remove the original function reference named add . The rest of the code is still runnable due to foo refers to the object for the function.
  • Result Explain: return_1()() looks wired at first. If we take a closer look, it is the same way as we are calling the function, return_1() is I_am_1 as it just returns this function. In this case, we can think return_1()()=1mentally, so it does not surprise us the foo(1,2) gives an output of 3. We can also pass foo(1,2) to another function. In this case, we passed it to itself. Since foo(1,2)=3 , the outer function will operate as foo(3,3) . To get the result, we can pass the entire function with its arguments over and let the program execute at runtime to decipher everything.

Code Visualisation

Pass Function to another Function | Image By Author
Pass Function to another Function | Image By Author

Python Decorators Structure

A Python decorator transforms the functionality of the original object without modifying the original one. It is syntax sugar that allows users to extend the object’s ability more conveniently instead of duplicating some existing code. Decorators are a pythonic implementation of the decorator design pattern (Note: python decorator isn’t precisely the same as decorator design pattern). 

We have discussed that we can return a function as a return type. Now, we extend that idea to demonstrate how a decorator works: we can return a function as a return type within another function.

To make our example more fun, we can create our decorator like a wrapper around a function and later stacks the multiple decorators. 

Let’s first define our functions and use the example of making some dumplings. 🥟🥟🥟

				
					## Python Decorators Basic -- I love dumplings
def add_filling_vegetable(func):
    def wrapper(*args, **kwargs):
        print("<^^^vegetable^^^>")
        func(*args, **kwargs)
        print("<^^^vegetable^^^>")
    return wrapper
def add_dumplings_wrapper(func):
    def wrapper(*args, **kwargs):
        print("<---dumplings_wrapper--->")
        func(*args, **kwargs)
        print("<---dumplings_wrapper--->")
    return wrapper
def dumplings(ingredients="meat"):
    print(f"({ingredients})")
add_dumplings_wrapper(add_filling_vegetable(dumplings))()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
				
			

Code walkthrough

Function Definition: we defined three functions called add_filling_vegetable, add_dumplings_wrapper, and dumplings . Within add_filling_vegetable and add_dumplings_wrapper, we define a wrapper function that wraps around the original function passed as an argument. Additionally, we can do additional things. In this case, we print some text before and after the original function. If the original function also defined parameters, we can pass them in from the wrapper function. We also leverage the magic *args and **kwargs to be more flexible

Result Explain

  • we can call the default ingredients for meat by using the default one add_dumplings_wrapper(add_filling_vegetable(dumplings))() , we can see the function are chaining together. It’s not readable, which we will fix shortly with decorator syntax. The core idea here is that we keep passing the function object as an argument to another. The function does two things: 1) continue doing the unmodified original function; 2) execute additional code. The execution of the entire code is like a round trip. 
add_dumplings_wrapper(add_filling_vegetable(dumplings))() | Image By Author
add_dumplings_wrapper(add_filling_vegetable(dumplings))() | Image By Author
  • for add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients=’tofu’) it changes the default ingredients from meat to tofu by passing an additional argument. In this case, *args and **kwargs is very useful for resolving the complexity. The wrapper functions don’t have to unwrap to know the context of the argument. As a decorator, it is safe to pass down the actual function without modifying them.
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients='tofu')
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients='tofu')

So far, we haven’t touched the decorator syntax yet. Let’s make a small change in how we define the original function and call it fancy_dumplings.

				
					## Stack decorator, the order matters here
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
    print(f"({ingredients})")
fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
fancy_dumplings(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
				
			

Now decorator simplifies how we call all the functions and makes it more readable. We only need to call fancy_dumplings only once. it is much cleaner visually than nesting multiple functions horizontally. 

What can I use decorators for?

Great! How to create the Python decorators are clear to us now. What can I use decorators for? 

There are many potential practical use cases for Python decorators. It makes your code easier to read and dynamically. Note you don’t necessarily need to go decorators. We can always implement a wrapper around another function to achieve the same goal. The decorator is just syntactic sugar.

You can build your time decorator to evaluate the performance for a given function. The following is a timer example from Primer on Python Decorators

				
					import functools
import time
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:.10f} secs")
        return value
    return wrapper_timer
				
			

It measures the time to execute a given function. Let’s stack it with our dumplings example.

				
					@timer
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
    print(f"({ingredients})")
fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
# Finished 'wrapper' in 0.0000334990 secs
				
			

You can keep stacking the decorators to achieve your goal by simply calling the original function without worrying about wrapping other functions around when calling, as we have provided the sequence of the decorator when we define the original function. 

Photo by Markus Spiske on Unsplash
Photo by Markus Spiske on Unsplash

Final Thoughts

There are many possibilities you can leverage the Python decorators. Essentially, it’s a wrapper to alert the behavior of the original function. It depends on your perspective to judge how practical the decorator is, but it shouldn’t be a syntax that feels foreign to you. I hope with some visualization, the concept of decorators becomes more straightforward to understand. Please let me know if you have any comments on this story. 

About Me

I hope my stories are helpful to you. 

For data engineering post, you can also subscribe to my new articles or becomes a referred Medium member that also gets full access to stories on Medium.

In case of questions/comments, do not hesitate to write in the comments of this story or reach me directly through Linkedin or Twitter.

More Articles

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Share via
Copy link