loading...

30 Days of Python πŸ‘¨β€πŸ’» - Day 13 - Decorators

arindamdawn profile image Arindam Dawn Updated on ・3 min read

30-days-of-python (30 Part Series)

1) 30 Days of Python πŸ‘¨β€πŸ’» - Day 1 - Introduction 2) 30 Days of Python πŸ‘¨β€πŸ’» - Day 2 - Data Types I 3 ... 28 3) 30 Days of Python πŸ‘¨β€πŸ’» - Day 3 - Data Types II 4) 30 Days of Python πŸ‘¨β€πŸ’» - Day 4 - Data Types III 5) 30 Days of Python πŸ‘¨β€πŸ’» - Day 5 - Conditions & Loops I 6) 30 Days of Python πŸ‘¨β€πŸ’» - Day 6 - Loops II & Functions 7) 30 Days of Python πŸ‘¨β€πŸ’» - Day 7 - Developer Environment 8) 30 Days of Python πŸ‘¨β€πŸ’» - Day 8 - OOP Basics 9) 30 Days of Python πŸ‘¨β€πŸ’» - Day 9 - OOP Pillars 10) 30 Days of Python πŸ‘¨β€πŸ’» - Day 10 - OOP Missing Pieces 11) 30 Days of Python πŸ‘¨β€πŸ’» - Day 11 - Functional Programming Basics 12) 30 Days of Python πŸ‘¨β€πŸ’» - Day 12 - Lambda Expressions & Comprehensions 13) 30 Days of Python πŸ‘¨β€πŸ’» - Day 13 - Decorators 14) 30 Days of Python πŸ‘¨β€πŸ’» - Day 14 - Error Handling 15) 30 Days of Python πŸ‘¨β€πŸ’» - Day 15 - Generators 16) 30 Days of Python πŸ‘¨β€πŸ’» - Day 16 - Module Basics 17) 30 Days of Python πŸ‘¨β€πŸ’» - Day 17 - External Modules 18) 30 Days of Python πŸ‘¨β€πŸ’» - Day 18 - File I/O 19) 30 Days of Python πŸ‘¨β€πŸ’» - Day 19 - Regular Expressions 20) 30 Days of Python πŸ‘¨β€πŸ’» - Day 20 - Debugging and Testing 21) 30 Days of Python πŸ‘¨β€πŸ’» - Day 21 - Scripting Basics 22) 30 Days of Python πŸ‘¨β€πŸ’» - Day 22 - Scripting Extras 23) 30 Days of Python πŸ‘¨β€πŸ’» - Day 23 - Web Scraping 24) 30 Days of Python πŸ‘¨β€πŸ’» - Day 24 - Web Development Basics 25) 30 Days of Python πŸ‘¨β€πŸ’» - Day 25 - Web Development Extras 26) 30 Days of Python πŸ‘¨β€πŸ’» - Day 26 - Machine Learning Basics 27) 30 Days of Python πŸ‘¨β€πŸ’» - Day 27 - ML & Data Science I 28) 30 Days of Python πŸ‘¨β€πŸ’» - Day 28- ML & Data Science II 29) 30 Days of Python πŸ‘¨β€πŸ’» - Day 29 - Automation Testing 30) 30 Days of Python πŸ‘¨β€πŸ’» - Day 30 - Free Python Resources

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.

def multiplier(num1, num2):
  return num1 * num2

some_variable = multiplier # (a reference to the function is created)

del multiplier # (deletes the function)

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
def logger(func, args):  # higher order function
    print(f'The result of the passed function is {func(*args)}')


def sum(num1, num2):
    return num1 + num2


logger(sum, (1, 5))
def random(): # Higher order function
  def special():
    print('I am something special')
  return special

random_value = random()
random_value() # I am something special
# One line way
random()() # I am something special

Custom Decorators

Now using the above principles, here is how a custom decorator would look like

def starmaker(func):
  '''
  A decorator function which accepts a function
  and then wraps some goodness into it and
  returns it back!
  '''
  def wrapper():
    func()
    print('You are a star now!')
    print('*********')
  return wrapper

@starmaker
def layman():
  print('I am just a layman')

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

def starmaker(func):
  '''
  A decorator function which accepts a function
  and then wraps some goodness into it and
  returns it back!
  '''
  def wrapper():
    func()
    print('You are a star now!')
    print('*********')
  return wrapper

def layman():
  print('I am just a layman')

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.

def emojifier(func):
  def wrapper(emoji):
    # kwags are keyword arguments
    print(emoji)
    func()
  return wrapper

@emojifier
def random():
  pass

random('πŸ˜€') # πŸ˜€

Why decorators are 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.

# Create an @authenticated decorator that only allows 
# the function to run is user1 has 'valid' set to True:
test_user = {
    'name': 'Jackson',
    'valid': True
}

another_user = {
  'name': 'Nathan',
  'valid': False
}

def authenticated(fn):
  def wrapper(*args, **kwargs):
    if args[0]['valid']:
      fn(args)
  return wrapper

@authenticated
def message_friends(user):
    print('message has been sent')

message_friends(test_user) # message has been sent
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:

That's all for today. Tomorrow I shall explore all about error handling techniques in Python. Another important topic ahead.

Till then,

Have a great one!

30-days-of-python (30 Part Series)

1) 30 Days of Python πŸ‘¨β€πŸ’» - Day 1 - Introduction 2) 30 Days of Python πŸ‘¨β€πŸ’» - Day 2 - Data Types I 3 ... 28 3) 30 Days of Python πŸ‘¨β€πŸ’» - Day 3 - Data Types II 4) 30 Days of Python πŸ‘¨β€πŸ’» - Day 4 - Data Types III 5) 30 Days of Python πŸ‘¨β€πŸ’» - Day 5 - Conditions & Loops I 6) 30 Days of Python πŸ‘¨β€πŸ’» - Day 6 - Loops II & Functions 7) 30 Days of Python πŸ‘¨β€πŸ’» - Day 7 - Developer Environment 8) 30 Days of Python πŸ‘¨β€πŸ’» - Day 8 - OOP Basics 9) 30 Days of Python πŸ‘¨β€πŸ’» - Day 9 - OOP Pillars 10) 30 Days of Python πŸ‘¨β€πŸ’» - Day 10 - OOP Missing Pieces 11) 30 Days of Python πŸ‘¨β€πŸ’» - Day 11 - Functional Programming Basics 12) 30 Days of Python πŸ‘¨β€πŸ’» - Day 12 - Lambda Expressions & Comprehensions 13) 30 Days of Python πŸ‘¨β€πŸ’» - Day 13 - Decorators 14) 30 Days of Python πŸ‘¨β€πŸ’» - Day 14 - Error Handling 15) 30 Days of Python πŸ‘¨β€πŸ’» - Day 15 - Generators 16) 30 Days of Python πŸ‘¨β€πŸ’» - Day 16 - Module Basics 17) 30 Days of Python πŸ‘¨β€πŸ’» - Day 17 - External Modules 18) 30 Days of Python πŸ‘¨β€πŸ’» - Day 18 - File I/O 19) 30 Days of Python πŸ‘¨β€πŸ’» - Day 19 - Regular Expressions 20) 30 Days of Python πŸ‘¨β€πŸ’» - Day 20 - Debugging and Testing 21) 30 Days of Python πŸ‘¨β€πŸ’» - Day 21 - Scripting Basics 22) 30 Days of Python πŸ‘¨β€πŸ’» - Day 22 - Scripting Extras 23) 30 Days of Python πŸ‘¨β€πŸ’» - Day 23 - Web Scraping 24) 30 Days of Python πŸ‘¨β€πŸ’» - Day 24 - Web Development Basics 25) 30 Days of Python πŸ‘¨β€πŸ’» - Day 25 - Web Development Extras 26) 30 Days of Python πŸ‘¨β€πŸ’» - Day 26 - Machine Learning Basics 27) 30 Days of Python πŸ‘¨β€πŸ’» - Day 27 - ML & Data Science I 28) 30 Days of Python πŸ‘¨β€πŸ’» - Day 28- ML & Data Science II 29) 30 Days of Python πŸ‘¨β€πŸ’» - Day 29 - Automation Testing 30) 30 Days of Python πŸ‘¨β€πŸ’» - Day 30 - Free Python Resources

Posted on by:

arindamdawn profile

Arindam Dawn

@arindamdawn

Software Engineer who loves building user interfaces. In the quest to learn, unlearn and re-learn things.

Discussion

markdown guide
 

Cool series, thanks for making your journey public! It's an interesting read :)

I believe the first example of a higher order function is incorrect, since sum(1,5) is evaluated and then passed as int to the logger function.

I changed it so that the args are passed to logger as a tuple and then unpacked. This way sum is passed as a function to logger.

def logger(func, args): # higher order function
  print(f'The result of the passed function is {func(*args)}')

def sum(num1, num2):
  return num1 + num2

logger(sum, (1,5))
 

Thanks a lot for the correction :) I have updated my code.
Very glad that you liked the series.