w3resource

Python Decorators: Understanding Function and Class Decorators

Introduction to Python Decorators

Decorators are a powerful feature in Python that allows you to modify or extend the behavior of functions or classes without changing their actual code. Decorators are widely used for logging, access control, instrumentation, and more.

This tutorial will focus on understanding function decorators and class decorators through examples, with minimal theory.

Function Decorators

Function decorators are functions that modify the behavior of other functions. They are defined using the @decorator_name syntax.

Example 1: Basic Function Decorator

Code:

# A simple decorator function
def simple_decorator(func):
    def wrapper():
        print("Before the function call")  # Code to execute before the function
        func()  # Call the original function
        print("After the function call")  # Code to execute after the function
    return wrapper  # Return the wrapper function

# Applying the decorator to a function
@simple_decorator
def say_hello():
    print("Hello!")

# Calling the decorated function
say_hello()

Output:

Before the function call
Hello!
After the function call

Explanation:

Decorator Function ('simple_decorator'):

  • The 'simple_decorator' function takes another function ('func') as its argument.
  • Inside it, the wrapper function is defined, which adds behavior before and after calling the original function.

Using the Decorator:

  • The '@simple_decorator' syntax is used to apply the decorator to the 'say_hello' function.
  • When 'say_hello' is called, it actually executes the wrapper function, printing messages before and after the original ‘say_hello’ function is called.

Example 2: Function Decorator with Arguments

Code:

# Decorator that accepts arguments
def decorator_with_args(func):
    def wrapper(*args, **kwargs):
        print("Arguments passed to the function:", args, kwargs)  # Log arguments
        result = func(*args, **kwargs)  # Call the original function
        print("Function result:", result)  # Log the result
        return result
    return wrapper

# Applying the decorator to a function
@decorator_with_args
def add(x, y):
    return x + y

# Calling the decorated function with arguments
add(10, 22)

Output:

Arguments passed to the function: (10, 22) {}
Function result: 32

Explanation:

Decorator with Arguments ('decorator_with_args'):

  • The wrapper function accepts '*args' and ‘**kwargs' to handle any number of positional and keyword arguments.
  • The decorator logs the arguments and the result of the function call.

Using the Decorator:

  • The '@decorator_with_args' syntax is used to apply the decorator to the add function.
  • When 'add(10,22)' is called, the decorator prints the arguments and the result.

Example 3: Function Decorator with Arguments for the Decorator

Code:

# Decorator factory that takes arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):  # Repeat the function call `times` times
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# Applying the decorator with an argument
@repeat(5)
def greet(name):
    print(f"Hello, {name}!")

# Calling the decorated function
greet("Philemon")

Output:

Hello, Philemon!
Hello, Philemon!
Hello, Philemon!
Hello, Philemon!
Hello, Philemon!

Explanation:

Decorator Factory ('repeat'):

  • The 'repeat' function takes an argument ('times') and returns a decorator ('decorator').
  • The 'wrapper' function calls the original function multiple times, depending on the value of times.

Class Decorators:

Class decorators modify or extend the behavior of classes in a similar way to function decorators.

Example 4: Basic Class Decorator

Code:

# A simple class decorator
def class_decorator(cls):
    class WrappedClass:
        def __init__(self, *args, **kwargs):
            self.instance = cls(*args, **kwargs)  # Instantiate the original class

        def __getattr__(self, name):
            return getattr(self.instance, name)  # Delegate attribute access to the original instance

        def new_method(self):
            return "This is a new method added by the decorator!"
    return WrappedClass

# Applying the decorator to a class
@class_decorator
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        return f"Hello, my name is {self.name} and I'm {self.age} years old."

# Creating an instance of the decorated class
person = Person("Sajra", 30)

# Calling methods
print(person.greet())             
print(person.new_method())

Output:

Hello, my name is Sajra and I'm 30 years old.
This is a new method added by the decorator!

Explanation:

Class Decorator ('class_decorator'):

  • The decorator function wraps the original class in a new class ('WrappedClass').
  • '__getattr__' is used to delegate attribute access to the original instance.
  • A new method 'new_method' is added to the class.

Using the Decorator:

  • The '@class_decorator' syntax is used to apply the decorator to the Person class.
  • The greet method behaves as usual, while the 'new_method' demonstrates the added functionality.

Example 5: Class Decorator with State

Code:

# Class decorator that adds state
def add_state(cls):
    class WrappedClass:
        def __init__(self, *args, **kwargs):
            self.instance = cls(*args, **kwargs)
            self.decorator_state = 0  # Initial state added by the decorator

        def __getattr__(self, name):
            return getattr(self.instance, name)

        def increment_state(self):
            self.decorator_state += 1  # Method to modify the state
            return self.decorator_state
    return WrappedClass

# Applying the decorator to a class
@add_state
class Calculator:
    def add(self, x, y):
        return x + y

# Creating an instance of the decorated class
calc = Calculator()

# Calling methods
print(calc.add(10, 25))             
print(calc.increment_state())      
print(calc.increment_state())

Output:

35
1
2

Explanation:

Class Decorator with State ('add_state'):

  • This decorator adds a new state (‘decorator_state’) to the original class.
  • It also provides a method (‘increment_state’) to modify and access this state.

Using the Decorator:

  • The '@add_state' syntax applies the decorator to the Calculator class.
  • The add method functions normally, while 'increment_state' manages the decorator's state.

Summary:

  • Function Decorators: Modify or extend functions' behavior. They can be simple or complex, handling arguments or controlling multiple function calls.
  • Class Decorators: Modify or extend classes' behavior, adding new methods, managing state, or wrapping the original class.
  • Common Patterns:
    • Wrapper Functions: Used to add behavior before and after the original function or class method.
    • Decorator Factories: Decorators that take arguments to control their behavior.

Decorators are a powerful tool in Python, allowing you to write cleaner, more maintainable, and more flexible code by abstracting repetitive or cross-cutting concerns.



Become a Patron!

Follow us on Facebook and Twitter for latest update.