Decorators

Decorators in Python provide a way to modify or extend the behavior of a function without changing its actual code. They are applied using the @decorator_name syntax above a function definition. Decorators are widely used for logging, access control, performance measurement, authentication, and many other tasks.

A decorator wraps another function and allows execution of additional code before or after the wrapped function runs. This helps in writing reusable functionality that can be applied to multiple functions.

1. Understanding Functions as First-Class Objects

In Python, functions are treated as objects. This means they can be stored in variables, passed as arguments, or returned from other functions. Decorators rely on this property.

Example

def greet():
   return "Hello"

x = greet
print(x())

Output:

Hello

The function greet is assigned to x, demonstrating that functions can be passed around like data.

2. Creating a Simple Decorator

A decorator is a function that takes another function as input and returns a new function.

Example: Basic Decorator

def my_decorator(func):
   def wrapper():
       print("Before function call")
       func()
       print("After function call")
   return wrapper

def say_hello():
   print("Hello")

decorated = my_decorator(say_hello)
decorated()

Output:

Before function call
Hello
After function call

Here, the original function say_hello() is wrapped with additional behavior.

3. Using the @ Syntax (Decorator Syntax)

Python provides a cleaner and more readable way to apply decorators using the @ symbol.

Example

def my_decorator(func):
   def wrapper():
       print("Before function")
       func()
       print("After function")
   return wrapper

@my_decorator
def show():
   print("Inside function")

show()

Output:

Before function
Inside function
After function

The decorator is applied directly to the show function.

4. Decorators with Arguments

If a function accepts arguments, the wrapper must also accept them and pass them forward.

Example: Decorator for Functions with Arguments

def log(func):
   def wrapper(a, b):
       print(f"Calling function with {a} and {b}")
       return func(a, b)
   return wrapper

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

print(add(5, 3))

Output:

Calling function with 5 and 3
8

5. Returning Values from Decorated Functions

Decorators should return the result of the wrapped function when needed.

Example

def square_decorator(func):
   def wrapper(n):
       result = func(n)
       return result * result
   return wrapper

@square_decorator
def get_number(n):
   return n

print(get_number(4))

Output:

16

6. Decorators with *args and kwargs

To support any number of arguments, decorators should use *args and **kwargs.

Example: Flexible Decorator

def logger(func):
   def wrapper(*args, **kwargs):
       print("Arguments:", args, kwargs)
       return func(*args, **kwargs)
   return wrapper

@logger
def multiply(a, b, c=1):
   return a * b * c

print(multiply(2, 3, c=4))

Output:

Arguments: (2, 3) {'c': 4}
24

7. Decorators with Arguments (Decorator Factory)

Sometimes a decorator needs its own arguments. In that case, we create a decorator function that returns another decorator.

Example

def repeat(times):
   def decorator(func):
       def wrapper():
           for _ in range(times):
               func()
       return wrapper
   return decorator

@repeat(3)
def greet():
   print("Hello")

greet()

Output:

Hello
Hello
Hello

8. Using functools.wraps

Without wraps(), a decorated function loses its original name and docstring. This can be fixed using functools.wraps.

Example

from functools import wraps

def my_decorator(func):
   @wraps(func)
   def wrapper():
       print("Wrapper executed")
       return func()
   return wrapper

@my_decorator
def hello():
   print("Hello")

print(hello.__name__)

Output:

Wrapper executed
Hello
hello

hello.__name__ stays as hello, not wrapper.