in

Python Decorators: Explained (With Examples and Use Cases)

Python decorators are an incredibly useful construct in Python. Using decorators in Python, we can modify the behavior of a function by wrapping it inside another function. Decorators enable us to write cleaner code and share functionality. This comprehensive guide will provide an in-depth explanation of not only how to use decorators but also how to create them.

What is a Python Decorator?

A Python decorator is a function that takes in another function as an argument, adds some kind of functionality to it, and returns a modified version of the original function.

In more technical terms, a decorator foo() is said to decorate a function bar() if:

  • foo() takes bar() as a parameter
  • foo() returns another callable function baz()
  • baz() is a modified version of bar() that contains additional logic

So foo() decorates bar() by returning baz() which has the same signature as bar() but enhanced functionality.

Here is some sample code to illustrate:

# Foo is a decorator, it takes in another function, bar as a parameter
def foo(bar):

  # Here we create baz, a modified version of bar
  # baz will call bar but can do anything before and after 
  def baz():

    # Before calling bar, we print something
    print("Something")  

    # Then we run bar by making a function call
    bar()

    # Then we print something else after running bar
    print("Something else")

  # Finally, foo returns baz, a modified version of bar
  return baz

So in summary:

  • foo() is the decorator function
  • bar() is the original function
  • baz() is the decorated version of bar() returned by foo()

This allows us to enhance bar() by wrapping it inside foo() which returns the enhanced baz().

How to Create a Decorator in Python

To illustrate how decorators are created and used in Python, let‘s walk through a simple example.

We will create a decorator function called create_logger() that will log the name of the function it decorates every time the decorated function is called.

Step 1: Define the Decorator Function

First, we need to define our decorator function. The decorator will take in the function we want to decorate as a parameter named func:

# Create the decorator function
def create_logger(func):

  # Function body will go here

Step 2: Define the Enhanced Function

Inside our decorator, we will define a nested function called modified_func() that will contain the additional logging logic:

# Inside create_logger
def modified_func():

  # Print function name    
  print("Calling: ", func.__name__)

  # Call the function being decorated
  func()

Step 3: Return the Enhanced Function

Finally, our decorator returns the enhanced function modified_func:

# Create the decorator
def create_logger(func):

  # Define the enhanced function
  def modified_func():
    print("Calling: ", func.__name__)
    func()

  # Return it
  return modified_func

And we have now created our create_logger() decorator function! It takes in a function func, enhances it by adding logging, and returns the modified version.

How to Use Decorators in Python

To use our new create_logger() decorator, we add @create_logger before the definition of the function we want to decorate:

@create_logger
def say_hello():
  print("Hello, World!")

Now whenever we call say_hello(), it will first print the message "Calling: say_hello" before executing the actual function body.

Here is what happens when we run say_hello():

Calling: say_hello  
Hello, World!

The @create_logger syntax allows us to easily decorate say_hello() with our decorator.

This is equivalent to manually calling the decorator as follows:

# Original function
def say_hello():
  print("Hello, World!")

# Manually call decorator  
say_hello = create_logger(say_hello)

So the @ syntax automatically applies the decorator function for us.

Slightly More Advanced Examples

The above example illustrated basic usage. Now let‘s look at some more advanced scenarios:

Decorating Functions with Arguments

If the function being decorated accepts arguments, then the wrapper function needs to accept arbitrary arguments and pass them when calling the wrapped function:

def foo(func):

  # Wrapper accepts arbitary args
  def wrapper(*args, **kwargs):

    # Do something before
    print("Before") 

    # Call function with received args
    func(*args, **kwargs)

    # Do something after
    print("After")

  return wrapper

For example:

@foo
def bar(x, y):
  print(x, y)

bar(1, 2)

This will output:

Before
1 2  
After

The wrapper has access to the arguments passed to the decorated function.

Decorating Classes

Python allows decorating classes too. The decorator will be applied to the __init__ method of the class:

def foo(func):

  def wrapper(*args, **kwargs):
    # Do something before init
    print("Creating object!")

    # Call init with received args
    func(*args, **kwargs)

  return wrapper

@foo
class Bar:
  def __init__(self):
    print("In init")

# Creating object! 
# In init

This can be useful for initializing classes by doing something before __init__ is called.

Real-World Examples of Decorators in Python

While you can create custom decorators like shown above, Python also provides some built-in decorators like @staticmethod, @classmethod, @property etc.

Let‘s look at some common real-world examples of using decorators:

@staticmethod

The @staticmethod decorator is used to create static methods in a class. Static methods can be called on a class without needing to instantiate it:

class Calculator:

  @staticmethod
  def add(x, y):
    return x + y

# Call static method  
result = Calculator.add(2, 3) 

Static methods can‘t access class state because they don‘t receive the class instance as first arg.

@classmethod

Similar to static methods, @classmethod can also be called on the class directly. But class methods receive the class as the first argument instead of an instance.

class Calculator:

  @classmethod
  def name(cls):
    return cls.__name__

print(Calculator.name()) # Prints Calculator

This allows class methods to access class state.

@property

The @property decorator is used to create properties in Python classes. This allows accessing methods like attributes:

class Person:

  def __init__(self, name):
    self._name = name

  @property
  def name(self):
    return self._name

p = Person(‘John‘)  
print(p.name) # Calls getter internally

We can also define setters using the @x.setter decorator to reassign values.

  @name.setter
  def name(self, new_name):  
    self._name = new_name

  p.name = ‘Mary‘ # Calls setter

@lru_cache

Python provides a built-in @lru_cache decorator in its functools module. This implements memoization and caching to prevent expensive function calls from running repeatedly:

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
  print(f"Computing fib({n})")
  return n if n < 2 else fib(n-1) + fib(n-2)

print(fib(7)) # Computes once
print(fib(7)) # Returns cached value

The maxsize parameter sets the limit on cached results. This decorator can significantly speed up programs using recursion or dynamic programming.

@wraps

Sometimes decorators modify the metadata of the wrapped function such as its docstring or name. The @wraps decorator from functools module preserves these metadata:

from functools import wraps

def foo(func):
  @wraps(func)
  def wrapper():
    # Do something
    return func()
  return wrapper

@foo
def bar():
  """bar docstring"""
  pass

print(bar.__name__) # bar
print(bar.__doc__) # bar docstring

This ensures that the wrapped function‘s metadata is preserved by the decorator.

Why are Decorators Important in Python?

Now that you have seen decorators in action, let‘s discuss why they are so useful in Python:

Enables Code Reuse

Decorators allow logic to be reused easily. The same decorator can be applied to multiple functions to avoid repetition and share functionality. For example, a caching decorator can easily be applied to compute-heavy functions to speed them up.

Cleaner and Modular Code

Decorators help divide code into smaller reusable pieces that focus on single concerns. The business logic stays in the function while cross-cutting concerns are handled separately by decorators. This makes code modular and cleaner.

Extend Functions without Modification

Decorators extend behavior of functions dynamically without needing to modify them. New features can be added to existing functions by applying decorators. This improves maintainability and reduces risk.

Implement Common Patterns

Many design patterns like logging, rate-limiting, authentication etc can be implemented cleanly using decorators. This avoids cluttering business logic with these cross-cutting concerns.

Widely Used in Python Libraries and Frameworks

Decorators enable libraries and frameworks like Flask, Django, pytest etc to provide extensibility. For example, Flask uses decorators to define routes, handle errors and secure views. Decorators are a natural fit for implementing middleware behaviors.

Overall, decorators make it easy to modify behavior on the fly and are integral to Python architecture and design. Mastering their usage is key to writing clean and Pythonic code.

Final Thoughts

We have covered a lot of ground on how decorators work in Python and how you can create and use them. Here are some key takeaways:

  • Decorators are functions that modify and extend other functions.
  • They allow logic to be added before and after calls to decorated functions.
  • Python provides some built-in decorators like @property, @staticmethod etc.
  • Custom decorators can be defined easily in Python by using nested wrapper functions.
  • Decorators are widely used in Python frameworks and enable cleaner code by separating concerns.

Decorators open up many possibilities for metaprogramming in Python. They are also powerful tools for implementing design patterns and behaviors like caching, logging, rate-limiting etc.

I hope this guide gave you a good understanding of how to use decorators effectively. They will become invaluable parts of your Python toolkit allowing you to write more modular and maintainable code.

Next you can read up on Python context managers which are another powerful construct in Python for resource management.

AlexisKestler

Written by Alexis Kestler

A female web designer and programmer - Now is a 36-year IT professional with over 15 years of experience living in NorCal. I enjoy keeping my feet wet in the world of technology through reading, working, and researching topics that pique my interest.