How to Decorate Your Code With Decorators

ljinLab
6 min readMay 4, 2022
https://pixabay.com/photos/shell-beach-snail-shell-maritime-1496269/

If you have some amount of experience programming with Python, I’m sure you saw an at-sign, @ (Koreans call this a sea snail because it LOOKS like a sea snail) here and there. This article explains what those sea snails are and how they are used. If you know basic Python and want to take your Python to the next level, read on!

What are decorators?

Before I go on and on about building and implementing a decorator, I’ll first explain what it is. Decorators, well, allow you to decorate a function. In other words, they can change little bits of functionalities. For example, let’s say you have a function that allows you to make comments on a blog post and you want to make it so that only people who have logged on to the website can leave comments on premium posts. How cumbersome would it be if you had to write a new function when you already have the core feature already built and ready to go?

This is where decorators come in. You can decorate a function so that the function you already wrote behaves a little bit differently.

Decorators come in extremely handy in programming and their use cases include timing, logging, and authentication.

Understanding Scope and Closure

Scope and closure are important concepts that you must understand before working with decorators. Fortunately, they are not difficult to understand.

Scope

Scope is the range of code where a value is valid. Let’s consider the following code.

# example.py
SALES_TAX = 0.3
def calculate_sales_tax(price):
return price * SALES_TAX

This simple code applies the SALES_TAX for a specific item. When you were first learning Python, you may have not noticed that the SALES_TAX is NOT written inside of the function. However, the code runs perfectly, nonetheless.

Let’s look at another example!

# example2.py
def calculate_sales_tax(price):
SALES_TAX = 0.3
return price * SALES_TAX
print(SALES_TAX) # NameError: name `SALES_TAX` is not defined

The two example snippets look almost identical. The only difference is where the SALES_TAX is defined. Where indeed! In the first snippet, the SALES_TAX was defined in the example.py scope. In other words, any code that is written in that file has access to the SALES_TAX variable.

In the second snippet, however, the SALES_TAX was defined inside of the calculate_sales_tax function. This means that the SALES_TAX variable is only valid inside of the calculate_sales_tax function. This is because of the concept of scopes. Scoping is simple; anything that you have in the higher scope, in our case, the file scope, can be used in the lower scope, in our case, the function scope. If this is confusing, think of it like try buying something real using Monopoly money. You can use real cash to buy real items as well as more Monopoly stuff, but if you try to buy something real using Monopoly money, people will look at you like you're crazy.

Closures

Now that we know that a variable defined inside of a function is only valid inside of the function. You may also wonder what if I enclose a function with another function?

def outer_function(some_number):
def inner_function():
print(some_number)
inner_function()

outer_function(10) # prints 10

This may look confusing and useless, but hang on with me for a moment. The some_number was supposed to be for outer_function, but the inner_function takes and uses some_number. Why? Because it can. Going back to the previous notion that anything that is defined in the higher scope can be used in the lower scope, anything that can be used in the outer_function scope can also be used in the inner_function scope.

We’re done playing now. Why on earth would this be useful?

This is useful because the inner function can access the values from the outer function even after we’re done with using the outer function. To make this more clear, let’s make a small change in the code.

def outer_function(some_number):
def inner_function():
print(some_number)
# changed it so that the function is returned, not called
return inner_function

ten_printer = outer_function(10)
ten_printer() # prints 10

This is fascinating for a couple of reasons. The value 10 was passed to the outer function, and the outer function was called on line 7. After line 7, the outer function has nothing to do with the rest of the script. The most important fact here is that the inner function has remembered the value 10 ( enclosed the value for terminology's sake) and still prints out 10!

We can call the ten_printer function as much as we want, and it will still remember the enclosed value. This behavior is eerily similar to a method in a simple class.

class TaxCalculator:
def __init__(self):
self.sales_tax = 0.3
def total_after_sales_tax(self, price):
return (self.sales_tax * price) + price

This class can be rewritten to be the following.

def sales_tax_calculator():
sales_tax = 0.3
def total_after_tax(price):
return (sales_tax * price) + price
return total_after_tax

stc = sales_tax_calculator()
stc(10) # prints 13
stc(40) # prints 52

Because the TaxCalculator class only has one method other than __init__ method, the second way of writing can be another arrow in your quiver and may be more elegant. This idea of enclosing a value from the outer scope is referred to as a closure, and it is a concept that we will be using to build our decorators.

Finally Decorators!

We have seen that a closure can be used to model an instance of a simple class by letting you use a value from the outer scope (lexical environment if you’re more comfortable with JavaScript jargon) inside of the inner function. Remember that the closure was a mere step to learning the ins and outs of a decorator — the sea snail.

As I mentioned before, the decorator allows you to modify the behavior of an existing function. Let’s take timing something for an example. Timing the running time of a function or a set of operations is simple in Python. You can either use the timeit module or surround the piece of code that you want to time with start_time and end_time and find the difference. However, if you were to time multiple pieces of codes, this process gets tedious very quickly and is a violation of the DRY Principle.

from datetime import datetime
def dumb_loop():
for i in range(1_000_000):
print(i, end=" ")

start_time = datetime.now()
dumb_loop()
end_time = datetime.now()
print(f"It took {end_time - start_time} to run the function.}")

This is a lot of work if it has to be repeated every time you want to time a function. The breakthrough here is that because Python treats functions as first class objects, they can be passed to another function as arguments. To continue from the timing example, we could write a function that times functions and pass the function that we want to time to the aforementioned function!

I feel like I’m saying functions way too much, but they’re fun to say. They literally have the word fun in them. Functions

def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = datetime.now()
func(*args, **kwargs)
end_time = datetime.now()
return end_time - start_time
return wrapper

dumb_loop_timer = timer_decorator(dumb_loop)
dumb_loop_timer()

In the example code above, the timer_decorator function takes in another function as its parameter. Next, we measure and return the time before and after running the function. Then, the outer function, the timer_decorator function, returns the wrapper like we did with closures.

Using the same logic, we can pass in any function to the timer_decorator and we can call the returned function to measure the time it took to run the function!

This pattern of programming is so ubiquitous in Python that Python even has syntactic sugar for such a pattern.

# this is same as saying dumb_loop = timer_decorator(dumb_loop)
@timer_decorator
def dumb_loop():
# same function definition

print(dumb_loop())

With this syntactic sugar, we can decorate our code and make it more extensible. Because we set the parameters to accept *args and **kwargs, we can even pass in arguments to the function that we are trying to decorate.

There are many great articles online that explain many, real-world use cases of the decorators, but it will be difficult to understand fully if you do not have an acceptable understanding of the scope and closures. With this article, I hope that I have given you enough background information to understand and implement your own decorators. The best way to learn anything is by doing; go ahead and decorate your code with these wonderful sea snails!

Originally published at https://www.ljinlabs.com/blog.

--

--

ljinLab

Programmer, GIS Enthusiast, Entrepreneur, Life long student. (personal website under construction)