Decorators in Python

The decorator’s feature in Python has a simple syntax for calling higher-order functions. A decorator is a function that takes another function and extends its behavior without explicitly modifying its code. Essentially, decorators provide a way to add or modify code in functions or methods, or even to modify the behavior of classes, in a clean and DRY (Don’t Repeat Yourself) way.

Basic Decorator Syntax:

Here’s the simplest form of a decorator:

def pythoncorner_decorator(func):
    def wrapper():
        print("Execute logic or code before function pythonCorner_func() is called.")
        pythonCorner_func()
        print("Execute logic or code after function pythonCorner_func() is called.")
    return wrapper

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

say_hello()

When say_hello() is called, it outputs:

Execute logic or code before function pythonCorner_func() is called.
Hello!
Execute logic or code after function pythonCorner_func() is called.

The @pythoncorner_decorator is Python’s decorator syntax. @pythoncorner_decorator is just an easier way of saying say_hello = my_decorator(say_hello). It’s up to you to apply a decorator to a function.

Decorators with Arguments:

Sometimes, it’s useful to pass arguments to your decorators.

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

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

greet("World")

Here @repeat(num_times=4) applies a decorator that repeats the function call 4 times.

Chaining Decorators:

You can apply an indefinite number of decorators to a function by applying them on top of each other.

@decorator1
@decorator2
@decorator3
def func():
    pass

This order means func() will be wrapped by decorator1, which will be wrapped by decorator2, which will be wrapped by decorator3.

Class-based Decorators:

Decorators can also be classes. They need to implement the __call__ method.

class MyDecorator:
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("Execute logic or code before function pythonCorner_func() is called.")
            func(*args, **kwargs)
            print("Execute logic or code after function pythonCorner_func() is called.")
        return wrapper

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

Decorators in the Standard Library:

Python’s decorators are used in various core modules and libraries. Some examples include:

  • @staticmethod: transforms a method or function into a static method.
  • @classmethod: Bind a method to a class rather than an instance.
  • @property: Create a read-only property method.
  • @functools.wraps: Adjusts the wrapper function’s metadata.
  • @functools.lru_cache: Cache the results of function calls.

Notes:

  • Decorators can sometimes make debugging harder since they “hide” the actual function.
  • Be mindful not to overuse them, making the code harder to understand.
  • Leverage them to abide by the DRY principle when there is genuine repeated logic across multiple functions or methods.

Decorators are a powerful and useful tool in Python since they allow programmers to modify the behavior of function or method calls clean and maintainable. They are widely used in Python web frameworks, testing frameworks, and other areas.

Certainly, here’s an example of some Python code using decorators:

Example 1: Basic Function Decorator

A simple decorator that prints logs before and after a function call.

def log_decorator(func):
    def wrapper():
        print(f"Calling function: {func.__name__}")
        func()
        print(f"Function {func.__name__} finished execution")
    return wrapper

@log_decorator
def hello_world():
    print("Hello, World!")

# Usage
hello_world()

Example 2: Decorator with Arguments

A decorator repeats the execution of the decorated function a specified number of times.

def repeat_decorator(num_times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat_decorator(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

# Usage
greet("Alice")

Example 3: Class-based Decorator

A class-based decorator to measure the execution time of a function.

import time

class TimerDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        result = self.func(*args, **kwargs)
        end_time = time.time()
        print(f"Function {self.func.__name__}  took {end_time - start_time:.2f} seconds to run")
        return result

@TimerDecorator
def slow_function():
    time.sleep(2)
    print("Finished Execution")

# Usage
slow_function()

Example 4: Decorator for Class Method

A decorator applied to a class method that ensures the method can be called only if certain conditions are met.

def is_positive(func):
    def wrapper(instance, value):
        if value < 0:
            raise ValueError("Value must be positive")
        return func(instance, value)
    return wrapper

class PositiveKeeper:
    def __init__(self):
        self._value = 0

    @is_positive
    def set_value(self, value):
        self._value = value

    def get_value(self):
        return self._value

# Usage
pk = PositiveKeeper()
pk.set_value(10)  # This should work
try:
    pk.set_value(-5)  # This should raise an error
except ValueError as ve:
    print(ve)

Example 5: Decorating a Property

A decorator to validate property assignments.

class Person:
    def __init__(self, age):
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

# Usage
p = Person(30)
try:
    p.age = -10  # This should raise an error
except ValueError as ve:
    print(ve)

These examples illustrate different uses of decorators in Python, including decorating functions, class methods, and properties, and defining decorators using both functions and classes. These can be useful in various scenarios like logging, enforcing access control, instrumentation, ensuring specific conditions, and more, which provides enhanced functionality in a clean and readable manner.

Leave a comment