HomepythonPython Decorators

Python Decorators

Introduction

Decorators are a powerful and flexible feature in Python that allow you to modify the behavior of functions or classes. They are often used to add functionality to existing code in a clean and readable way. This article provides a comprehensive guide to understanding and using decorators in Python, including their purpose, how they work, and practical examples of their application.

What is a Decorator?

A decorator is a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate.

In Python, decorators are implemented as functions (or classes) that take another function (or class) as an argument and extend or alter its behavior. This is often achieved using the @decorator syntax.

Basic Function Decorators

Let’s start with a simple example to understand how function decorators work.

Example 1: Basic Decorator

  1. Define a Decorator Function:
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper
  1. Use the Decorator:
@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

In this example, my_decorator is a function that takes another function func as an argument. Inside my_decorator, a nested function wrapper is defined and returned. The wrapper function adds additional behavior before and after calling the original func.

Decorators with Arguments

Often, you need decorators that can accept arguments. This requires an extra layer of nesting.

Example 2: Decorator with Arguments

  1. Define a Parameterized Decorator:
def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator_repeat
  1. Use the Decorator:
@repeat(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:

Hello, Alice!
Hello, Alice!
Hello, Alice!

In this example, repeat is a decorator factory that takes an argument num_times and returns a decorator. The decorator, in turn, returns a wrapper function that calls the original function num_times times.

Decorating Methods in Classes

Decorators can also be applied to methods within classes. The process is similar, but you need to consider the self parameter.

Example 3: Method Decorators

  1. Define a Method Decorator:
def log_method_call(func):
    def wrapper(self, *args, **kwargs):
        print(f"Calling method {func.__name__}")
        result = func(self, *args, **kwargs)
        print(f"Method {func.__name__} completed")
        return result
    return wrapper
  1. Use the Decorator in a Class:
class MyClass:
    @log_method_call
    def say_hello(self):
        print("Hello!")

obj = MyClass()
obj.say_hello()

Output:

Calling method say_hello
Hello!
Method say_hello completed

In this example, log_method_call is a decorator for class methods that logs the start and end of a method call.

Built-in Decorators

Python provides several built-in decorators that are commonly used:

  1. @staticmethod:
  • Defines a method that doesn’t access or modify the class state.
class MyClass:
    @staticmethod
    def my_static_method():
        print("This is a static method.")
  1. @classmethod:
  • Defines a method that takes the class itself as the first argument.
class MyClass:
    @classmethod
    def my_class_method(cls):
        print(f"This is a class method of {cls}.")
  1. @property:
  • Used to define getters and setters in a class.
class MyClass:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if isinstance(new_value, int):
            self._value = new_value
        else:
            raise ValueError("Value must be an integer.")

Chaining Decorators

You can apply multiple decorators to a single function by stacking them.

Example 4: Chaining Decorators

def bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))

Output:

<b><i>Hello, Alice!</i></b>

In this example, greet is first decorated with italic and then with bold, resulting in the output being wrapped in both <i> and <b> tags.

Practical Use Cases for Decorators

  1. Logging:
  • Automatically log function calls and their parameters.
import logging

def log_call(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Calling {func.__name__} with args {args} and kwargs {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a, b):
    return a + b

add(1, 2)
  1. Timing:
  • Measure the execution time of functions.
import time

def time_it(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time} seconds")
        return result
    return wrapper

@time_it
def compute():
    return sum(range(1000000))

compute()
  1. Access Control:
  • Restrict access to certain functions.
def require_auth(func):
    def wrapper(user, *args, **kwargs):
        if not user.is_authenticated:
            raise PermissionError("User must be authenticated")
        return func(user, *args, **kwargs)
    return wrapper

class User:
    def __init__(self, is_authenticated):
        self.is_authenticated = is_authenticated

@require_auth
def view_dashboard(user):
    return "Dashboard"

user = User(is_authenticated=True)
print(view_dashboard(user))

By understanding and utilizing decorators, you can implement cross-cutting concerns such as logging, access control, and performance monitoring in a clean and efficient manner. With the concepts and examples provided in this article, you should be well-equipped to start using decorators in your own Python projects.

Subscribe
Notify of

0 Comments
Inline Feedbacks
View all comments

Popular