Python Decorators

Python decorators are a powerful feature that can make your code cleaner, smarter, and more reusable.

In this guide, we will explore what are python decorators are, how they work, and real-world examples you can use right away. Whether you are new to decorators or want to master advanced use cases like memorization or singleton patterns, this post has you covered.

What Are Python Decorators?

Python decorators are the function that wraps another function to extend or change their behavior. Decorators are useful when you want to reuse logic across multiple functions, such as logging, timing, or access control.

Think of it like a wrapper you put around a gift that does not change the gift, but it makes it look nicer or adds something extra.

Here’s what decorators usually do:

  • They take a function as input.
  • They add new behavior to that function
  • They return a new function with the added features

For example, instead of editing many functions to log their output, you can write one decorator that logs everything, then apply it to each function easily using the @decorator_name syntax. This approach makes your code cleaner, shorter, and easier to maintain.

Here’s a simple 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()

Output:

Before function call
Hello!
After function call

The @my_decorator syntax is just a shorthand for writing say_hello = my_decorator(say_hello).

Understanding Function Decorators

Function decorators are most commonly used for enhancing or monitoring function behavior.

Example: Logging User Activity

def log_activity(func):
    def wrapper(*args, **kwargs):
        print(f"Running {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_activity
def process_data():
    print("Processing data...")

Timeit Python Decorator

A Timeit decorator in Python is used to measure how long a function takes to run. It helps you to test the performance of your code by timing the execution of any function.

You can easily create a custom decorator using the time module. This is helpful when you want to compare the speed of different functions or optimize slow parts of your code.

Here’s how it works:

  • The decorator records the start time before the function runs.
  • It runs the function.
  • After the function finishes, it records the end time.
  • Then, it prints or returns the total time taken.

You can measure how long a function takes to run:

import time

def timeit(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Function '{func.__name__}' took {end - start:.4f} seconds.")
        return result
    return wrapper

@timeit
def slow_function():
    time.sleep(2)

slow_function()

This example will print how many seconds slow_function() took to run. You can use this pattern to time any function in your project.

Python Decorator with Arguments

A Python decorator with arguments is a special kind of decorator that allows you to pass parameters to the decorator itself. Normally, decorators wrap a function and add extra behavior.

When you use a decorator with arguments, you add an extra layer so the decorator can be more flexible.

For example, you might want to log messages at different levels (like “INFO” or “ERROR”) or set a timeout value and decorator arguments make this possible.

Here’s how it works in simple steps:

  1. You define a function that accepts arguments (this acts like a decorator factory).
  2. Inside that function, you define the actual decorator.
  3. Inside the decorator, you define a wrapper function that runs around the original function.
  4. You return the wrapper from the decorator and the decorator from the outer function.

Example:

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

@repeat(3)
def say_hello():
    print("Hello!")

say_hello()  # This will print "Hello!" three times

In this example, repeat(3) is the decorator with the argument 3, which tells Python to run say_hello() three times. This shows how you can use decorator arguments to make your code more reusable and customizable.

Using Python Classmethod and Setter Decorators

@classmethod

The @classmethod decorator lets you define a method that works with the class itself, not just instances of the class. This means you can access or change class-level data.

You use cls (short for “class”) instead of self. It’s useful when you want a method that affects all objects of a class.

class MyClass:
    count = 0

    @classmethod
    def increment(cls):
        cls.count += 1

@property and @setter

The @property decorator lets you access a method like it’s a variable. Then, @setter allows you to set the value of that property safely perfect for data validation or logging changes.

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value > 0:
            self._price = value
        else:
            raise ValueError("Price must be positive")

Decorator for Class Python (Class-Based Decorators)

In Python, a class-based decorator is a decorator that’s written using a class instead of a function. Just like function-based decorators, it adds extra behavior to functions or methods.

But instead of defining it with def, you use a class and define the __call__ method inside it. This allows you to maintain state (i.e., store data) between calls, which is useful in more complex use cases.

Here’s how class-based decorators work:

  • When the function is called, the class’s __call__ method runs instead.
  • Python calls the class when the decorated function is defined.
  • The class receives the function and stores it.
class LogExecution:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"Calling {self.func.__name__}")
        return self.func(*args, **kwargs)

@LogExecution
def do_work():
    print("Work done.")

You can also pass arguments to the class constructor if needed.

Memoization and Caching in Python

Memoization is a way to speed up your program by remembering the results of expensive function calls and reusing them when the same inputs happen again.

Python has a built-in tool for this called functools.lru_cache. You just need to add a decorator to your function, and Python will handle the rest. It works great when your function gets called often with the same arguments.

Using functools.lru_cache

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Caching stores the results of a function so your program doesn’t need to repeat the same work. You can:

  • Use lru_cache for simple function caching.
  • Use dictionaries manually for custom caching logic.
  • Use external libraries like joblib or diskcache for more control or when working with large data.

Both techniques help reduce computation time and improve performance, especially in data-heavy or recursive tasks.

Custom Memoize Python Decorators

A custom memoize decorator in Python is a special function that helps your code to run faster by remembering the results of expensive function calls.

Instead of running the same function again with the same input, it simply returns the stored result. This is very helpful when working with functions that take time, like those that do a lot of calculations or access data from slow sources (like APIs or databases).

It works as:

  • You create a decorator function that stores results in a dictionary.
  • When the decorated function is called, it checks if the result is already in the dictionary.
  • If it is, it returns the saved result without running the function again.
  • If not, it runs the function and stores the result for future use.

Example:

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@memoize
def slow_function(n):
    print(f"Calculating {n}...")
    return n * n

This custom memoize decorator is useful for improving performance, especially in functions that are called often with the same inputs.

Python Singleton Decorator

A Python Singleton Decorator is a special design pattern used to make sure a class has only one instance during the program’s runtime.

In simple terms, no matter how many times you create an object from that class, it always gives you the same object. This can be useful when you want a single shared resource like a database connection, configuration manager, or logging system.

You can use a decorator to implement this pattern in a clean and reusable way. Here’s how it works:

How the Singleton Decorator works:

  • It wraps around a class.
  • It checks if an instance of the class already exists.
  • If it exists, it returns the existing instance.
  • If it doesn’t, it creates one and saves it.
def singleton(cls):
    instances = {}
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyDatabase:
    pass

db1 = MyDatabase()
db2 = MyDatabase()

print(db1 is db2)  # True

In the above code, both db1 and db2 refer to the same object. That’s the magic of the singleton decorator, it controls how many times your class gets instantiated.

Create Python Decorators from Scratch

Lets create decorator in python from scratch:

Step 1: Write a Normal Function

This is the function you want to decorate (add extra behavior to).

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

Step 2: Create a Decorator Function

This is a new function that takes another function as input.

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

Step 3: Apply the Decorator

Use @my_decorator right above the function you want to decorate.

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

Step 4: Call the Function

Now just call the function like normal.

say_hello()

What Will Happen?

When you run this, it prints:

Before function runs  
Hello!  
After function runs

What Did You Do?

You added extra behavior before and after your original function without changing it. That’s the power of a decorator!

Let’s build a decorator that counts how many times a function runs:

def count_calls(func):
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        print(f"Call #{wrapper.calls} to {func.__name__}")
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def greet():
    print("Hello!")

Best Practices When Using Python Decorators

  • Always use functools.wraps to preserve function metadata.
  • Keep the decorator logic small and focused.
  • Use class-based decorators for complex behavior.
  • Document your decorators clearly.

Conclusion

Python decorators are more than just a neat syntax trick. They help you to write cleaner, more efficient, and more reusable code. From logging to caching, decorators can significantly improve the quality of your projects.

Whether you need a python timer decorator, a python singleton decorator, or a classmethod decorator python, you now have a wide range of tools at your disposal. Start small, explore with real examples, and build your own custom python decorators as your skills grow.

Stay ahead of the curve with the latest insights, tips, and trends in AI, technology, and innovation.

LEAVE A REPLY

Please enter your comment!
Please enter your name here