Python Decorators

A tutorial on how to use python decorator syntax for cleaner coding

Anuradha Wickramarachchi
Level Up Coding

--

Python comes with many syntactic artefacts that enable developers to build applications faster and most importantly, with clean code. If you’re a programmer, you know how important code quality and reliability is! So in this article, I will give a brief introduction to python decorators. If you’re interested in a cleaner and efficient coding, have a look at my following article.

Before we start using python decorators, we need to understand how Python functions work. Python functions are considered as first-class functions, which means they can be treated as objects and passed around at your will.

Python can have functions defined within functions, known as inner functions. A function can also be returned from another function (This is one way of implementing a switch operator in Python).

Photo by Markus Spiske on Unsplash

Function’s Applications as an OOP Object

Switch case implementation

Python dictionary is an object construction where an object will be returned to a key that it is referred with. Since Python does not have an explicit switch operator, we use the dict construct to make one. See this example.

op_switch = {
'sqr': lambda x: x**2,
'sqrt': lambda x: x**0.5,
'abs': lambda x: abs(x)
}

Our switch case is based on a string to pick the operation. The dictionary returns a function. I have used lambda function definitions for code simplicity. They behave similarly to that of functions (not exactly the same!). They can be accessed as follows.

>>> switch['sqr'](12)
144
>>> switch['sqrt'](25)
5.0

Passing a function to another function

Consider a situation where you need to wrap another function. Imagine, the wrapper function can be shared across many other functions. Before I tell you about a real work example, let’s follow this pet scenario.

Imagine you need to have a function that will either execute the function and return an answer. If an exception is thrown, None will be returned.

def deco_function(func, *args):
try:
return func(*args)
except:
print("Error occured")
return None

def divide(a, b):
return a/b

Our deco_function function will execute the passed function with the set of args passed as *args. I have omitted keyword arguments for simplicity. If we run this, we’ll see the following output for each of the following parameters we give.

>>> deco_function(divide, 10, 2)
5.0
>>> deco_function(divide, 10, 0)
Error occured

Pretty neat right!

The main problem with this approach is that the function signature for the wrapper function must be well known. We need to pass the function itself and the arguments. This is less maintainable in a complex scenario!

Introducing Python Decorators

The same function above can be sugar-coated with much better syntax using python decorators syntax. Let’s see how.

def deco_function(func):
def wrapped(*args):
"""
This is the wrapper for a function to be fail safe
"""
try:
return func(*args)
except:
print("Error occured")
return None
return wrapped

@deco_function
def divide(a, b):
"""
This is a function to divide two numbers
"""
return a/b

In this example, we decorate the divide function with deco_function decorator. Within the decorator, a wrapper is placed around the passed function and returns the wrapper. This is similar to the following statement.

divide = deco_function(divide)

However, we now have the liberty to forget about the call_function implementation. That is pretty neat!

The real world decorator usage!

Screenshot by Author taken from Flask Website

In case you aren’t familiar with a decorator use case; let’s have a look at the Flask server.

Flask is a server implementation for python. The screenshot displays how routing is implemented in Flask using decorators. We only have to mention the route that the function shall be activated. So far we did not discuss how parameters can be passed on to decorators. We’ll discuss that soon!

Proper use of decorators and function names

One thing to remember is, wrapping a function can cause confusions about its identity. This is because the function is no longer itself once we wrap it with something else.

>>> divide.__name__
wrapped
>>> print(divide.__doc__)
This is the wrapper for a function to be fail safe

The __name__ attribute of a function returns the function name itself and printing __doc__ returns the docstring. However, we can see that for both of these attributes we get the values from the wrapper function, not from the referred function. This can cause severe confusion in large software. Here’s how to fix it.

import functoolsdef deco_function(func):
@functools.wraps(func)
def wrapped(*args):
"""
This is the wrapper for a function to be fail safe
"""
try:
return func(*args)
except:
print("Error occured")
return None
return wrapped

@deco_function
def divide(a, b):
"""
This is a function to divide two numbers
"""
return a/b

Note the code in bold. We import functools and decorate our wrapper. This functiontools.wraps decorator injects the docstring and name attributes to the wrapper so that we get the proper attributes when we print __name__ and __doc__.

>>> print(divide.__name__)
divide
>>> print(divide.__doc__)
This is a function to divide two numbers

Understanding how arguments and keyword arguments work

Python accepts ordered arguments followed by keyword arguments. This can be demonstrated as follows.

import functoolsdef print_args(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
args_arr = [(n, a) for n, a in enumerate(args)]
kwargs_arr = [(k, v) for k, v in kwargs.items()]
for k, v in args_arr + kwargs_arr:
print(k, v)
return wrapped

@print_args
def test_function(*args, **kwargs):
return a/b

Calling the above test_function function gives the following result.

>>> test_function('name', 'age', height=150, weight=50)
0 name
1 age
height 150
weight 50

Following the above observation, it must be noted that arguments are organized before keyword arguments. The order must be preserved in the arguments and it does not matter in keyword arguments. In a wrapper, both arguments and keyword arguments must be passed into the function being wrapped. This ensures the wider usability of the wrapper.

Decorators with Arguments

Now that we have a clear understanding of how decorators work, let’s see how we can use decorators with arguments.

def powered(power):
def powered_decorator(func):
def wrapper(*args):
return func(*args)**power
return wrapper
return powered_decorator
@powered(2)
def add(*args):
return sum(args)

In the above example, we have a wrapper with an attribute. Simply, this wrapper asks the answer to be raised into a power designated by the attribute. Few more examples where you might find parameterized decorators are as follows.

  1. Validating input fields, JSON strings, file existence, etc.
  2. Implementing switch cased decorators

Advanced Usecases of Decorators

Decorators can be used in a similar manner for classes. However, here we can talk about two ways we can use decorators; within a class, and for a class.

Decorators within a Class

In the following example, I use a decorator on functions of the class Calculator. This helps me to gracefully obtain a value for an operation when it fails.

import functoolsdef try_safe(func):
@functools.wraps(func)
def wrapped(*args):
try:
return func(*args)
except:
print("Error occured")
return None
return wrapped
class Calculator:

def __init__(self):
pass

@try_safe
def add(self, *args):
return sum(args)

@try_safe
def divide(self, a, b):
return a/b

The above code can be used as follows.

>>> calc = Calculator()
>>> calc.divide(10, 2)
5.0

Decorators for a Class

Using a decorator for a class will activate the decorator during the instantiation of the function. For example, the following code will check for graceful creation of the object with the constructor parameters. Should the operation fail, None will be returned in place of the object from Calculator class.

import functoolsdef try_safe(cls):
@functools.wraps(cls)
def wrapped(*args):
try:
return cls(*args)
except:
print("Error occured")
return None
return wrapped
@try_safe
class Calculator:

def __init__(self, a, b):
self.ratio = a/b

Injecting State for a Function using Decorators

During the process of wrapping a function, a state could be injected into the function. Let’s see the following example.

import functoolsdef record(func):
@functools.wraps(func)
def wrapped(*args):
wrapped.record += 1
print(f"Ran for {wrapped.record} time(s)")
return func(*args)
wrapped.record = 0
return wrapped
@record
def test():
print("Running")

Running the above example gives us the following output.

>>> test()
Ran for 1 time(s)
Running
>>> test()
Ran for 2 time(s)
Running
>>> test()
Ran for 3 time(s)
Running

This is useful in creating a singleton using decorators. Let’s see that next.

Singleton using Python Decorators

Singleton refers to an instance that is shared between calls, and would not duplicate for any reason. In simple terms, at first, an instance is created. In the following calls to make an instance, the existing instance will be returned.

Let’s see how can we implement a singleton using decorators.

import functoolsdef singleton(cls):
@functools.wraps(cls)
def wrapped(*args, **kwargs):
if not wrapped.object:
wrapped.object = cls(*args, **kwargs)
return wrapped.object
wrapped.object = None
return wrapped
@singleton
class SingularObject:
def __init__(self):
print("The object is being created")

We can confirm the functionality as follows.

>>> first = SingularObject()
The object is being created
>>> second = SingularObject()
>>> second is first
True

The objects refer to the same instance. Hence, we are guaranteed that no more objects are created. Thus a singleton!

Making a Wrapper/Decorator Class

So far we only considered functions as wrappers or decorators. However, in an OOP program, classes may be preferred. We can make this happen with just a few modifications to our record example. Let’s see the code.

import functoolsclass Record:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.record = 0
def __call__(self, *args, **kwargs):
self.record += 1
print(f"Ran for {self.record} time(s)")
return self.func(*args, **kwargs)
@Record
def test():
print("Run")

Note the sections in bold. We are using functools to update the function attributes. We use __call__ overload to define the action on a function call. The constructor __init__ initiates the variables similar to that we did after the wrapper function in the previous stateful decorator example. The outputs for the above example are as follows.

>>> test()
Ran for 1 time(s)
Run
>>> test()
Ran for 2 time(s)
Run
>>> test()
Ran for 2 time(s)
Run

Concluding Remarks

In this article, I showed how decorators can be used to implement wrappers in a much simpler and clean way. Following are a few examples you might use wrappers in your next project.

  1. Implement a routing mechanism, for example, in AWS lambda functions
  2. API caching mechanism, where you might want to keep track of the last few function calls
  3. Debounce in an IoT application (restricting function calls that happen within a very short period of time)

Let me know if you could think of any other creative applications that you may have used decorators. I’d love to hear them.

I hope you enjoyed this article. Cheers!

--

--