DEV Community

Tandap Noel Bansikah
Tandap Noel Bansikah

Posted on • Updated on

Decorators in Python.

Decorators are a powerful tool in Python for adding custom functionality to classes and functions. They are defined to classes and functions. They are defined a similar way to functions, but with the addition of a decorator keyword. In this article, we will be learning when? to use a decorator and why we need decorators with some few examples.

What are decorators?

A decorator is a function that is attached to another function or class. It takes one or more arguments, and its job is to modify the behavior of the original function or class.

Before we begin on how to create a decorator, we need to understand the fundamentals of functions before we dive right in. For more understanding about functions, you can visit this article Understanding functions in Python.
So let's create our functions.

def func1():
    print("Called func1")
func1()
Enter fullscreen mode Exit fullscreen mode

Results:

Called func1
Enter fullscreen mode Exit fullscreen mode

Now rather than print func1 or calling func1, we will print func1 without he brackets and see what happens.

def func1():
    print("Called func1")
print(func1)
Enter fullscreen mode Exit fullscreen mode

Output:

<function func1 at 0x000002CB9EEA3E20>
Enter fullscreen mode Exit fullscreen mode

You can see some random memory address, what this actually means is that: func1 is an object and since it is an object, we can pass it all round our program. To understand this better, let's create another function called func2.

def func1():
    print("Called func1")
def func2(f):
    f()
func2(func1)
Enter fullscreen mode Exit fullscreen mode

the function func2 has been created and it takes, and argument called f which is called below the function. Now since func1 is an object and we can represent functions as object let us see the output below.

Called func1
Enter fullscreen mode Exit fullscreen mode

You can see clearly that func1 is called, now how this works is that func1 is and object that represents the function func1() and we can pass it through parameters we can store them in variables and so on.

The above example is the basic principle that we need to understand in python when it comes to functions.
Now we are going to see a kind of function called wrapper functions.
Let us see the example below.

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper
Enter fullscreen mode Exit fullscreen mode

Essentially, what this function is doing is that it has another function defined inside it called wrapper, it prints out an output called started, called the function func as we have passed into our func1 and it will print out another value that says Ended. This function returns the wrapper function that has been defined inside our initial function.

Let's create another function called f and what we want to do is always do this func1 functionality our initial function using the newly created function called f.

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
func1(f)()
Enter fullscreen mode Exit fullscreen mode

and we have the output:

Started
Hello
Ended
Enter fullscreen mode Exit fullscreen mode

This might be a little bit complicated but the reason we us parenthesis () to call our function is because the value wrapper is a function. To understand this better, let's use the print to output the value of func1 so that you will see.

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
print(func1(f))
Enter fullscreen mode Exit fullscreen mode

and you can see the output:

<function func1.<locals>.wrapper at 0x000001C74A78ADD0>
Enter fullscreen mode Exit fullscreen mode

so, this tells us that when you call func1, you have a value that is actually equal to another function f.
This Property is what is actually going to allow us to what is called decorate a function.

Using function aliasing, we can also have the same output as below, Let's see the example below.

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
x = func1(f)
x()
Enter fullscreen mode Exit fullscreen mode

output:

Started
Hello
Ended
Enter fullscreen mode Exit fullscreen mode

What the above code actually does is that we have set x which is a variable = the function func1 which has the parament f passed into it. So that is function aliasing, changing the name of function without changing its functionality.

You might be wondering why this article is on decorators and we are talking more about functions, well if you have understood what we have learned so far, then you pretty much understand decorators.
So, the code above with the line x = func1(f) can be replaced with decorators. Let's see the code below.

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

@func1
def f():
    print("Hello")
f()
Enter fullscreen mode Exit fullscreen mode

What the code above does is to automatically write this line of code x = func1(f) for us every time we call f and we still have the same results.

Started
Hello
Ended
Enter fullscreen mode Exit fullscreen mode

Now let's make a slide modification to our code to show you something wrong with the way we have been doing things

def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

@func1
def f(a):
    print(a)
f("Hi")
Enter fullscreen mode Exit fullscreen mode

and that is: notice what happens when we add a parameter to our function, and rather than printing Hello it will be printing a and we can call f with the value "Hi"

Traceback (most recent call last):
  File "C:\Users\Bansikah\PycharmProjects\MyCompiler\decorator.py", line 15, in <module>
    f("Hi")
TypeError: func1.<locals>.wrapper() takes 0 positional arguments but 1 was given

Enter fullscreen mode Exit fullscreen mode

It give and error, notice that in our wrapper function.

def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper
Enter fullscreen mode Exit fullscreen mode

we call the function func() without any parameter, so to fix this we will use args and kwargs in the parameters of our wrapper function. You must have probably seen this in your python lesson but if you haven't then i am going to write another article on that.

def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        func(*args, **kwargs)
        print("Ended")

    return wrapper


@func1
def f(a):
    print(a)
f("Hi")
Enter fullscreen mode Exit fullscreen mode

The reason we are using the args and kwargs is that we know that our wrapper function is supposed to contain some certain number of arguments and we don't know if those arguments are keyword arguments or regular in-place arguments all we know is that it needs to have some arguments. This actually allows us to have any number of arguments on any specific decorated function and this will work properly.
Output:

Started
Hi
Ended
Enter fullscreen mode Exit fullscreen mode

And we have gotten the desired output we expected, and even if we decide to add another variable as below.

def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        func(*args, **kwargs)
        print("Ended")

    return wrapper


@func1
def f(a, b=9):
    print(a, b)
f("Hi")
Enter fullscreen mode Exit fullscreen mode

output:

Started
Hi 9
Ended
Enter fullscreen mode Exit fullscreen mode

this continuous to work and that is what the args and kwargs can do for us.

The final thing we are going to talk about is returning values from decorated functions, before that I will just like to say this: if the variables or function names we are using are confusing to you, you can change them to your own names that you will understand better.

So far what we have been doing is printing the values in the decorated functions, but we would like to return those values instead? Well, this is pretty much easy to do, we are going to do this by creating another function as in the example below.

def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        val = func(*args, **kwargs)
        print("Ended")
        return val
    return wrapper


@func1
def f(a, b=9):
    print(a, b)


# f("Hi")

@func1
def add(x, y):
    return x + y


print(add(4, 5))
Enter fullscreen mode Exit fullscreen mode

What we have done in the above code is that we created a function called add and it takes in two parameters x and y and also we created a variable val in the wrapper function and returned the val. You might be wondering that why didn't we just return the func(*args, **kwargs) like this: return func(*args, **kwargs) well it is better to do it this way.
output:

Started
Ended
9
Enter fullscreen mode Exit fullscreen mode

So that is essentially how we can return values from a decorated function. In the above example you can see that you can use a decorator on more than one function.
For better understanding, let's see the applications of decorators in real world examples.

#Python decorators - Example 1

def before_after(func):
    def wrapper(*args):
        print("Before")
        func(*args)
        print("After")
    return wrapper
class Test:
    @before_after
    def decorated_method(self):
        print("run")
t = Test()
t.decorated_method()
Enter fullscreen mode Exit fullscreen mode

output:

Before
run
After
Enter fullscreen mode Exit fullscreen mode

Another example here, the timer decorator, a very common example maybe you have seen that before.

Python Decorators - Example 2
import time
def timer(func):
    def wrapper():
        before = time.time()
        func()
        print("Function took:", time.time() - before, "seconds")
    return wrapper

@timer
def run():
    time.sleep(2)
run()
Enter fullscreen mode Exit fullscreen mode

output:

Function took: 2.0019941329956055 seconds
Enter fullscreen mode Exit fullscreen mode

it measures how long a function takes to run.
And lastly the log() decorator example.



# Python Decorators - Example 3

import datetime


def log(func):
    def wrapper(*args, **kwargs):
        with open("logo.txt", "a") as f:
            f.write("Called function with" + " ".join([str(arg) for arg in args]) + "at" + str(datetime.datetime.now()) + "\n")
        val = func(*args, **kwargs)
        return val

    return wrapper


@log
def run(a, b, c=9):
    print(a + b + c)


run(1, 3, c=9)

Enter fullscreen mode Exit fullscreen mode

output:

13
Enter fullscreen mode Exit fullscreen mode

and we have this inside out log.txt file.

Called function with1 3atCalled function with1 3at2023-06-05 01:00:47.423933
Called function with1 3at2023-06-05 01:02:38.287020
Enter fullscreen mode Exit fullscreen mode

That was it about decorators in python a very advanced and broad topic in python, congrats if you understood everything and it is really going to help you in becoming an expert in python. Please do well to ask you valuable questions in the comments section, your reactions will really encourage me to continue writing articles like this. Adios

Top comments (0)