Python Decorators
A tutorial on how to use python decorator syntax for cleaner 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).
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!
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.
- Validating input fields, JSON strings, file existence, etc.
- 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 wrappedclass 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 = 0def __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.
- Implement a routing mechanism, for example, in AWS lambda functions
- API caching mechanism, where you might want to keep track of the last few function calls
- 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!