30 Days Of Python 👨‍💻 - Day 13 - Decorators

This article is a part of a 30 day Python challenge series. You can find the links to all the previous posts of this series here
Today I explored an interesting topic, Decorators. I did apply a couple of decorators while trying out Object-Oriented Programming in Python such as @classmethod and @staticmethod, however, I did not go through them in details back then.

Decorators are a programming pattern. Decorators are simply functions in disguise.
Using decorators, it is possible to add more functionality to functions or super-charge them.
I will try to explain in my own lucid terms how they work under the hood and why they can be useful.
A lot of cool Python libraries makes extensive use of decorators and makes it feel as if they are magical. However, to understand decorators, some concepts need to be understood.

Functions as first-class citizens

Functions are first-class citizens in Python. What it basically means is that functions can be assigned to variables just like other data types and they can be passed as parameters to functions just like other values. In the JavaScript world too, functions have a similar behaviour so I already have this concept in my mental model.
  1. def multiplier(num1, num2):  
  2.   return num1 * num2  
  4. some_variable = multiplier # (a reference to the function is created)  
  6. del multiplier # (deletes the function)  
  8. print(some_variable(2,4)) # 8 (still able to call the function!)  
This ability to pass functions as values is essential for the creation of decorators in Python.

Higher-Order Functions

A function is called a higher-order function when,
  • It accepts another function as arguments (parameters)
  • It returns another function
  • Both
  1. def logger(func, args):  # higher order function  
  2.     print(f'The result of the passed function is {func(*args)}')  
  5. def sum(num1, num2):  
  6.     return num1 + num2  
  9. logger(sum, (15))  
def random()
# Higher order functiondefspecial(): print ('I am something special') returns special random_value = random()
  1. def random(): # Higher order function  
  2.   def special():  
  3.     print('I am something special')  
  4.   return special  
  6. random_value = random()  
  7. random_value() # I am something special  
  8. # One line way  
  9. random()() # I am something special  
Custom Decorators
Now using the above principles, here is how a custom decorator would look.
  1. def starmaker(func):  
  2.   ''''' 
  3.   A decorator function which accepts a function 
  4.   and then wraps some goodness into it and 
  5.   returns it back! 
  6.   '''  
  7.   def wrapper():  
  8.     func()  
  9.     print('You are a star now!')  
  10.     print('*********')  
  11.   return wrapper  
  13. @starmaker  
  14. def layman():  
  15.   print('I am just a layman')  
  17. layman()  
The starmaker decorator function gave super-powers to the layman function. It basically added a wrapper over the function. Now, this decorator @starmaker can be added on top of any function and that function would become a star! Very cool indeed.
Python interpreter recognizes the @decoratorname and converts it into a function in real-time and processes it. The above code is exactly similar to the following block without using the @decorator syntax
  1. def starmaker(func):  
  2.   ''''' 
  3.   A decorator function which accepts a function 
  4.   and then wraps some goodness into it and 
  5.   returns it back! 
  6.   '''  
  7.   def wrapper():  
  8.     func()  
  9.     print('You are a star now!')  
  10.     print('*********')  
  11.   return wrapper  
  13. def layman():  
  14.   print('I am just a layman')  
  16. starmaker(layman)() # This is the underlying decorator magic!  
I was initially quite confused when I came across decorators. However after demystifying their underlying principle, it became second nature and I was able to add it to my mental model.
If we compare it with the JavaScript universe, then JavaScript does not have decorators as a part of the language. However, TypeScript, which is a superset of JavaScript, has this concept of decorators. Frameworks like Angular, NestJs relies heavily on decorators.
A decorator function can also accept arguments and can be customized based on the passed arguments.
  1. def emojifier(func):  
  2.   def wrapper(emoji):  
  3.     # kwags are keyword arguments  
  4.     print(emoji)  
  5.     func()  
  6.   return wrapper  
  8. @emojifier  
  9. def random():  
  10.   pass  
  12. random('😀'# 😀  

Why are decorators useful?

Decorators are an important programming pattern and if used wisely, can provide a lot of benefits. It makes code very reusable and binds added functionality to functions, hence keeping code DRY.
  1. # Create an @authenticated decorator that only allows   
  2. # the function to run is user1 has 'valid' set to True:  
  3. test_user = {  
  4.     'name''Jackson',  
  5.     'valid'True  
  6. }  
  8. another_user = {  
  9.   'name''Nathan',  
  10.   'valid'False  
  11. }  
  13. def authenticated(fn):  
  14.   def wrapper(*args, **kwargs):  
  15.     if args[0]['valid']:  
  16.       fn(args)  
  17.   return wrapper  
  19. @authenticated  
  20. def message_friends(user):  
  21.     print('message has been sent')  
  23. message_friends(test_user) # message has been sent  
  24. message_friends(another_user) # (Does nothing)  
The above-authenticated decorator function only invokes the message_friends function based on the specified condition. This gives a lot of flexibility and performs conditional operations based on the status of the user’s authentication.
Reference articles to know more about decorators in Python
  • https://www.programiz.com/python-programming/decorator
  • https://realpython.com/primer-on-python-decorators/
That’s all for today. Tomorrow I shall explore all about error handling techniques in Python. Another important topic ahead.
Until  then,
Have a great one!