🐍 Python Q10 / 170

What are decorators in Python?

AI-Powered Answer ✓ Answered

Decorators in Python are a powerful and elegant way to modify or enhance the behavior of functions or methods without permanently altering their source code. They are essentially functions that take another function as an argument, add some functionality, and return the modified function.

What are Decorators?

At their core, a decorator is a design pattern that allows you to wrap another function to extend or alter its functionality without explicitly modifying the function itself. They are often used for cross-cutting concerns like logging, timing, authentication, caching, and more, making your code cleaner and more reusable.

Basic Syntax

Python uses the @ symbol as syntactic sugar for applying decorators. When you define a function and immediately precede it with @decorator_name, you are telling Python to pass the function to decorator_name and assign the result back to the original function name.

python
def my_decorator(func):
    def wrapper():
        print("Something is happening BEFORE the function is called.")
        func() # Call the original function
        print("Something is happening AFTER the function is called.")
    return wrapper

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

say_hello()
# Output:
# Something is happening BEFORE the function is called.
# Hello!
# Something is happening AFTER the function is called.

How Decorators Work Under the Hood

The @my_decorator syntax is just a shortcut. The line @my_decorator above def say_hello(): is equivalent to writing say_hello = my_decorator(say_hello) immediately after defining say_hello. The decorator function my_decorator takes say_hello as an argument, and returns a new function (the wrapper), which then replaces the original say_hello.

python
def my_decorator(func):
    def wrapper():
        print("Manual: Something is happening BEFORE.")
        func()
        print("Manual: Something is happening AFTER.")
    return wrapper

def say_goodbye():
    print("Goodbye!")

# Manually applying the decorator
say_goodbye = my_decorator(say_goodbye)
say_goodbye()
# Output:
# Manual: Something is happening BEFORE.
# Goodbye!
# Manual: Something is happening AFTER.

Common Use Cases

  • Logging: To log function calls, arguments, and return values.
  • Timing: To measure the execution time of a function.
  • Authentication/Authorization: To restrict access to functions based on user permissions or roles.
  • Caching: To store the results of expensive function calls and return the cached result for subsequent calls with the same arguments.
  • Retry Logic: To automatically retry a function call a certain number of times if it fails.
  • URL Routing in Web Frameworks: Frameworks like Flask and Django use decorators to associate URLs with view functions.

Decorators with Arguments

Sometimes, you need to pass arguments to your decorator itself (e.g., how many times to retry an operation). To achieve this, you need an extra layer of function wrapping. The outermost function takes the decorator arguments, and it returns the actual decorator function which then takes the function to be decorated.

python
def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result # Only return the result of the last call
        return wrapper
    return decorator_repeat

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

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Chaining Decorators

You can apply multiple decorators to a single function by stacking them. The order matters: the decorator closest to the function definition is applied first (or 'innermost'), then the next one up, and so on (working from bottom to top).

python
def decorator1(func):
    def wrapper(*args, **kwargs):
        print("Decorator 1 pre-call")
        result = func(*args, **kwargs)
        print("Decorator 1 post-call")
        return result
    return wrapper

def decorator2(func):
    def wrapper(*args, **kwargs):
        print("Decorator 2 pre-call")
        result = func(*args, **kwargs)
        print("Decorator 2 post-call")
        return result
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Original function call")

my_function()
# Output:
# Decorator 1 pre-call
# Decorator 2 pre-call
# Original function call
# Decorator 2 post-call
# Decorator 1 post-call

Key Takeaways

  • Decorators are functions that take another function as input, add functionality, and return a modified function.
  • They use the @ syntax as syntactic sugar for function = decorator(function).
  • They typically return a new 'wrapper' function that encapsulates the original function call, allowing for code to run before and after.
  • They are incredibly useful for implementing cross-cutting concerns (logging, timing, authentication) cleanly and without modifying the core logic of your functions.
  • Decorators can accept arguments themselves and can be chained together for multiple layers of modification.