杰瑞科技汇

decorators python

Of course! Let's break down Python decorators from the ground up, from a simple analogy to powerful, real-world examples.

decorators python-图1
(图片来源网络,侵删)

The Core Idea: An Analogy

Imagine you have a plain function, like a plain cup of coffee.

def coffee():
    print("Here's your plain coffee.")

Now, you want to add things to it: milk and sugar. You could do this directly inside the function, but that would make the function messy and not reusable.

A decorator is like a barista who takes your plain coffee function and adds milk and sugar to it without changing the original recipe of the coffee itself.

The barista (decorator) wraps the coffee (function) to add extra features (milk, sugar).

decorators python-图2
(图片来源网络,侵删)

The Problem Decorators Solve: Modifying Functions

Before decorators, if you wanted to add functionality to a function, you had a few options, all with drawbacks:

  1. Modify the function directly: This makes the function bloated and violates the Single Responsibility Principle.
  2. Create a wrapper function: You could create a new function that calls the original one.

Let's look at option 2. Imagine we want to add logging to our coffee function.

def coffee():
    print("Here's your plain coffee.")
# We want to log when this function starts and ends.
# Let's create a wrapper.
def log_coffee():
    print("Logging: Starting coffee function.")
    coffee()  # Call the original function
    print("Logging: Finished coffee function.")
log_coffee()

Output:

Logging: Starting coffee function.
Here's your plain coffee.
Logging: Finished coffee function.

This works, but it's not elegant. What if we have 10 functions we want to log? We'd have to write 10 different log_... wrappers. This is repetitive and violates the DRY (Don't Repeat Yourself) principle.

Decorators are the solution! They let you create this "wrapper" logic once and apply it to any function you want.


How Decorators Work: The "Syntactic Sugar"

A decorator is simply a function that takes another function as an argument and returns a new function that usually extends or modifies the behavior of the original function.

Let's build our logging_decorator from scratch.

Step 1: The Decorator Function

This function will accept a function (func) as its argument. Inside it, we define a "wrapper" function that contains our new logic (the logging). The wrapper function then calls the original func.

def logging_decorator(func):
    # This is the wrapper function that will replace the original function
    def wrapper():
        print(f"Logging: You are about to call '{func.__name__}'.")
        func()  # Call the original function
        print(f"Logging: You have finished calling '{func.__name__}'.")
    # The decorator returns the wrapper function
    return wrapper

Step 2: Applying the Decorator

Now, we can use the symbol to apply our decorator to our coffee function. This is just "syntactic sugar" for coffee = logging_decorator(coffee).

@logging_decorator
def coffee():
    print("Here's your plain coffee.")
@logging_decorator
def tea():
    print("Here's your plain tea.")

Step 3: Calling the Decorated Function

When you call coffee(), you are no longer calling the original coffee function. You are calling the wrapper function that was returned by logging_decorator.

print("--- Calling coffee() ---")
coffee()
print("\n--- Calling tea() ---")
tea()

Output:

--- Calling coffee() ---
Logging: You are about to call 'coffee'.
Here's your plain coffee.
Logging: You have finished calling 'coffee'.
--- Calling tea() ---
Logging: You are about to call 'tea'.
Here's your plain tea.
Logging: You have finished calling 'tea'.

As you can see, we successfully added logging to two different functions without changing their source code!


Handling Arguments

What if our functions need arguments? The wrapper function must accept *args and **kwargs to handle any number of positional and keyword arguments.

Let's create a timer decorator that can work with any function.

import time
def timer_decorator(func):
    def wrapper(*args, **kwargs): # Accept any arguments
        start_time = time.time()
        result = func(*args, **kwargs) # Pass the arguments to the original function
        end_time = time.time()
        print(f"'{func.__name__}' took {end_time - start_time:.4f} seconds to run.")
        return result # Return the result of the original function
    return wrapper
@timer_decorator
def slow_function(seconds):
    print(f"Sleeping for {seconds} seconds...")
    time.sleep(seconds)
    print("Done sleeping.")
slow_function(2)

Output:

Sleeping for 2 seconds...
Done sleeping.
'slow_function' took 2.0021 seconds to run.

Notice how wrapper(*args, **kwargs) captures the arguments (2 in this case) and passes them along to func(*args, **kwargs). It also returns the result of the original function, which is crucial.


The functools.wraps Helper

There's one problem with our decorators: they "hide" the original function's metadata.

@logging_decorator
def coffee():
    """This is a docstring for the coffee function."""
    print("Here's your plain coffee.")
print(coffee.__name__)  # Output: wrapper
print(coffee.__doc__)   # Output: None

This is bad because tools that inspect functions (like help(), debuggers, etc.) see the wrapper's metadata instead of the original function's.

The solution is to use functools.wraps. It's a decorator that you apply to your wrapper function to copy the original function's metadata (__name__, __doc__, etc.) over to the wrapper.

import time
import functools
def timer_decorator(func):
    @functools.wraps(func) # Apply this to the wrapper
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"'{func.__name__}' took {end_time - start_time:.4f} seconds to run.")
        return result
    return wrapper
@timer_decorator
def slow_function(seconds):
    """This function sleeps for a given number of seconds."""
    time.sleep(seconds)
print(slow_function.__name__) # Output: slow_function
print(slow_function.__doc__)  # Output: This function sleeps for a given number of seconds.

Best Practice: Always use @functools.wraps on your inner wrapper function.


Real-World Examples

Decorators are everywhere in Python frameworks like Flask, Django, and FastAPI.

Example 1: Authentication (Flask/Django)

A common use case is to check if a user is logged in before allowing them to access a certain part of a website.

# A mock function to check if the user is authenticated
def is_authenticated():
    # In a real app, this would check a session or token
    return False 
def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not is_authenticated():
            print("Access denied. Please log in.")
            return None # Or redirect to a login page
        return func(*args, **kwargs)
    return wrapper
@login_required
def view_dashboard():
    print("Welcome to your dashboard! Here are your stats.")
view_dashboard()

Output:

Access denied. Please log in.

Example 2: Caching (Memoization)

A performance decorator that stores the results of a function call. If the function is called again with the same arguments, it returns the cached result instead of re-computing it.

import functools
def cache(func):
    cached_results = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cached_results:
            print(f"Computing for {args}...")
            cached_results[args] = func(*args)
        else:
            print(f"Returning cached result for {args}...")
        return cached_results[args]
    return wrapper
@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # Computes values
print(fibonacci(10)) # Returns from cache
print(fibonacci(5))  # Returns from cache (if already computed)

Decorators with Arguments

What if you want to pass arguments to the decorator itself? This is a bit more advanced. You have to create a "decorator factory"—a function that returns the actual decorator.

Let's create a repeat decorator that takes an argument n to specify how many times to run a function.

import functools
def repeat(n):
    # This is the actual decorator
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
@repeat(n=3)
def say_hello(name):
    print(f"Hello, {name}!")
say_hello("Alice")

Output:

Hello, Alice!
Hello, Alice!
Hello, Alice!

Here's how it works:

  1. Python sees @repeat(n=3). It immediately calls the repeat function with n=3.
  2. repeat(n=3) returns the decorator function.
  3. Now, it's as if you wrote @decorator. The decorator function is then called with say_hello as its argument, and it returns the wrapper function.
  4. Finally, say_hello is bound to this wrapper function.

Summary

Concept Description
What is a decorator? A function that takes another function, adds some functionality, and returns a new function.
Why use them? To modify or extend behavior of functions/methods in a clean, reusable way (DRY principle).
The symbol Syntactic sugar for my_function = my_decorator(my_function).
*`argsandkwargs` Essential for decorators to work with functions that have arguments.
functools.wraps A must-have decorator to preserve the original function's metadata.
Real-world use Logging, timing, authentication, caching, access control, registering routes (Flask/Django).
分享:
扫描分享到社交APP
上一篇
下一篇