The Decorator Pattern is a design pattern that allows us to dynamically add new behavior to an object by wrapping it in another object that has the same interface as the original object. In Python, the Decorator Pattern is implemented using the concept of function decorators. In this tutorial, we will explore how to use the Decorator Pattern in Python with the help of function decorators.


What is a Function Decorator?

A function decorator is a function that takes another function as an argument and returns a new function that adds some additional functionality to the original function. In Python, function decorators are denoted by the '@' symbol followed by the name of the decorator function. Here's an example:

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

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

In this example, we define a function my_decorator that takes another function func as an argument and returns a new function wrapper that adds some additional functionality to the original function. We then use the @my_decorator syntax to apply the my_decorator function as a decorator to the say_hello function. When we call the say_hello function, it is actually the wrapper function that gets called, which adds the "Before function call" and "After function call" print statements around the original "Hello!" print statement.


How to Implement the Decorator Pattern with Function Decorators

Now that we understand how function decorators work, let's see how we can use them to implement the Decorator Pattern. The Decorator Pattern involves creating a base object, which can be a class or a function, and then creating one or more decorator functions that wrap around the base object to add additional behavior. Each decorator function takes the base object as an argument and returns a new object that wraps around the base object. Here's an example:

def base_function():
    print("Base function")

def decorator1(func):
    def wrapper():
        print("Decorator 1 - Before function call")
        func()
        print("Decorator 1 - After function call")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2 - Before function call")
        func()
        print("Decorator 2 - After function call")
    return wrapper

# Apply decorator1 and decorator2 to base_function
base_function = decorator1(decorator2(base_function))

# Call the decorated function
base_function()

In this example, we define a base function base_function that prints "Base function". We then define two decorator functions decorator1 and decorator2 that take a function as an argument and return a new function that adds some additional functionality to the original function. We apply these decorators to the base_function by calling decorator1(decorator2(base_function)), which returns a new function that first applies decorator2 and then applies decorator1.

When we call the resulting base_function with base_function(), it is actually the wrapper function returned by decorator1(decorator2(base_function)) that gets called. This wrapper function adds the "Decorator 2 - Before function call" and "Decorator 2 - After function call" print statements around the "Base function" print statement, and then passes the result to the wrapper function returned by decorator1, which adds the "Decorator 1 - Before function call" and "Decorator 1 - After function call" print statements.


Using Class Decorators to Implement the Decorator Pattern

While function decorators are the most common way to implement the Decorator Pattern in Python, it is also possible to use class decorators to achieve the same result. In this approach, we create a base class that defines the interface for the object we want to decorate, and then create one or more decorator classes that inherit from the base class and add additional behavior. Here's an example:

class BaseClass:
    def operation(self):
        print("Base operation")

class Decorator1(BaseClass):
    def __init__(self, obj):
        self.obj = obj

    def operation(self):
        print("Decorator 1 - Before operation")
        self.obj.operation()
        print("Decorator 1 - After operation")

class Decorator2(BaseClass):
    def __init__(self, obj):
        self.obj = obj

    def operation(self):
        print("Decorator 2 - Before operation")
        self.obj.operation()
        print("Decorator 2 - After operation")

# Create a decorated object
decorated_obj = Decorator1(Decorator2(BaseClass()))

# Call the decorated object's operation method
decorated_obj.operation()

In this example, we define a base class BaseClass that defines the interface for the object we want to decorate. We then define two decorator classes Decorator1 and Decorator2 that inherit from the BaseClass and add additional behavior to the operation method. Each decorator class takes an instance of the BaseClass as an argument and stores it in the obj instance variable.

We create a decorated object by creating an instance of Decorator1 that takes an instance of Decorator2 that takes an instance of BaseClass as an argument. This creates a chain of objects, where each decorator wraps around the previous decorator until we reach the base object.

When we call the operation method on the decorated_obj, it is actually the operation method of the innermost decorator that gets called first. This method adds the "Decorator 2 - Before operation" and "Decorator 2 - After operation" print statements around the operation method of the BaseClass object stored in the obj instance variable. The result is then passed to the operation method of the outermost decorator, which adds the "Decorator 1 - Before operation" and "Decorator 1 - After operation" print statements.


Conclusion

In this tutorial, we explored how to use the Decorator Pattern in Python with the help of function decorators and class decorators. The Decorator Pattern allows us to dynamically add new behavior to an object by wrapping it in another object that has the same interface as the original object. Function decorators are the most common way to implement the Decorator Pattern in Python, but class decorators can also be used for more complex scenarios. By using the Decorator Pattern, we can achieve a high degree of flexibility and modularity in our code, making it easier to add new functionality and maintain existing code.