杰瑞科技汇

Python装饰器究竟如何实现?

Of course! Let's break down Python decorators from the ground up. They are a powerful and elegant feature in Python, but they can seem tricky at first.

Python装饰器究竟如何实现?-图1
(图片来源网络,侵删)

The Core Idea: Functions as First-Class Citizens

To understand decorators, you first need to understand that in Python, functions are "first-class citizens." This means you can:

  1. Assign a function to a variable.
  2. Pass a function as an argument to another function.
  3. Return a function from another function.

This last point is the key to decorators.

def say_hello():
    print("Hello!")
# 1. Assigning a function to a variable
greet = say_hello
greet()  # Output: Hello!
# 2. Passing a function as an argument
def run_function(func):
    print("Running the function you passed in:")
    func()
run_function(say_hello) # Output: Running the function you passed in: \n Hello!
# 3. Returning a function from another function
def create_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier
times_3 = create_multiplier(3)
print(times_3(10)) # Output: 30

What is a Decorator?

A decorator is a function that takes another function as an argument (the "decorated" function), adds some kind of functionality, and then returns a new function.

It's essentially a wrapper that lets you extend the behavior of a function without permanently modifying it.

Python装饰器究竟如何实现?-图2
(图片来源网络,侵删)

Let's build a simple one. Imagine we want to print a message before and after another function runs.

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
def say_whee():
    print("Whee!")
# Now, let's decorate say_whee
say_whee = my_decorator(say_whee)
# Call the decorated function
say_whee()

Output:

Something is happening before the function is called.
Whee!
Something is happening after the function is called.

What happened?

  1. We defined say_whee.
  2. We passed say_whee to my_decorator.
  3. my_decorator returned the wrapper function.
  4. We assigned this returned wrapper function back to the say_whee variable.
  5. Now, when we call say_whee(), we are actually calling the wrapper() function, which contains the extra logic.

The Syntax (Syntactic Sugar)

Manually reassigning the function like say_whee = my_decorator(say_whee) is a bit clunky. Python provides a much cleaner way to do this using the symbol.

This is called the "pie" syntax. It does the exact same thing as the manual assignment.

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper
@my_decorator
def say_whee():
    print("Whee!")
say_whee()

The output is identical. The @my_decorator line above the function definition is just a shortcut for say_whee = my_decorator(say_whee).


Decorators with Arguments

What if the function we want to decorate takes arguments? For example, say_hello(name).

Our current wrapper function doesn't take any arguments. We need to make it more flexible. We can use *args and **kwargs to accept any number of positional and keyword arguments.

def do_twice(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)  # Call the original function with its arguments
        func(*args, **kwargs)  # Call it a second time
    return wrapper
@do_twice
def greet(name):
    print(f"Hello, {name}")
greet("Alice")

Output:

Hello, Alice
Hello, Alice

Here, *args captures "Alice" and **kwargs would capture any keyword arguments (like greet(name="Alice")).


Preserving Metadata with functools.wraps

When you decorate a function, it "forgets" its original name and docstring. This can be problematic for debugging and documentation.

def do_twice(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper
@do_twice
def greet(name):
    """This function greets a person."""
    print(f"Hello, {name}")
print(greet.__name__) # Output: wrapper
print(greet.__doc__)  # Output: None

To fix this, use the @functools.wraps decorator. It's a decorator that you apply to your wrapper function to "copy" the metadata from the original function.

import functools
def do_twice(func):
    @functools.wraps(func) # <-- Add this!
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper
@do_twice
def greet(name):
    """This function greets a person."""
    print(f"Hello, {name}")
print(greet.__name__) # Output: greet
print(greet.__doc__)  # Output: This function greets a person.

Best Practice: Always use @functools.wraps when you write your own decorators.


Practical Examples

Example 1: Timing a Function

A classic use case for decorators is performance monitoring.

import time
import functools
def timer(func):
    """Print the runtime of the decorated function."""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value # Return the result of the original function
    return wrapper_timer
@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum(range(num_times))
waste_some_time(10000)

Output:

Finished 'waste_some_time' in 0.0123 secs

Example 2: Checking for User Permissions

def admin_required(func):
    """Only allows admin users to access the decorated function."""
    @functools.wraps(func)
    def wrapper_user_check(user, *args, **kwargs):
        if user.get('role') != 'admin':
            raise PermissionError("User does not have admin privileges.")
        return func(user, *args, **kwargs)
    return wrapper_user_check
@admin_required
def delete_database(user, db_name):
    print(f"Admin {user['name']} is deleting database {db_name}.")
current_user = {'name': 'Alice', 'role': 'user'}
admin_user = {'name': 'Bob', 'role': 'admin'}
# This will fail
try:
    delete_database(current_user, 'my_app_db')
except PermissionError as e:
    print(e)
# This will succeed
delete_database(admin_user, 'my_app_db')

Output:

User does not have admin privileges.
Admin Bob is deleting database my_app_db.

Decorators with Arguments

What if you want to pass arguments to the decorator itself? For example, @repeat(num_times=3).

This requires an extra layer of nesting. The structure becomes: decorator_with_args(arg1, arg2)(func)

  1. The outermost function (repeat) takes the arguments.
  2. It returns a new decorator (actual_decorator).
  3. This new decorator then takes the function (func) and returns the wrapper.
import functools
def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat
@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")
greet("World")

Output:

Hello World
Hello World
Hello World
Hello World

Summary

Concept Description Example
First-Class Functions Functions can be passed as arguments, returned from other functions, and assigned to variables. def f(): pass; g = f
Basic Decorator A function that wraps another function to add behavior. def my_decorator(func): ...
Syntax A clean, syntactic shortcut for applying a decorator. @my_decorator def f(): ...
*`args, kwargs` Makes a decorator work with functions that take any arguments. def wrapper(*args, **kwargs): ...
functools.wraps Preserves the original function's name, docstring, and other metadata. @functools.wraps(func)
Decorator with Args A decorator factory that takes arguments. Requires an extra layer of nesting. def repeat(n): def decorator(func): ...
分享:
扫描分享到社交APP
上一篇
下一篇