我们来深入、全面地解析一下 Python 装饰器。这是一个强大且优雅的特性,但初学者常常觉得它有些神秘。

1. 核心概念:什么是装饰器?

装饰器(Decorator) 的本质是一个函数,它的作用是扩展或修改另一个函数(或类)的行为,而不需要修改其源代码。它提供了一种基于语法糖 @decorator_name 的清晰、可重用的方式来实现这一点。

你可以把它想象成给你现有的函数“穿上”一件“外衣”或“装备”,为其增加新的功能(如日志记录、权限校验、性能测试等)。


2. 一切皆对象:理解装饰器的基础

要理解装饰器,必须先掌握 Python 中以下几个核心概念:

  1. 函数是“一等公民”(First-Class Objects)
  • 函数可以像变量一样被赋值
  • 函数可以作为参数传递给另一个函数。
  • 函数可以作为另一个函数的返回值
  • 函数可以定义在另一个函数内部(嵌套函数)。
  1. 闭包(Closure)
  • 如果一个内部函数引用了外部函数的变量(包括参数),那么这个内部函数就是一个闭包。
  • 闭包可以“记住”并访问其词法作用域(定义时的作用域)的变量,即使外部函数已经执行完毕。

装饰器就是结合了“高阶函数”和“闭包”的语法应用。


3. 从零开始,手动实现一个装饰器

让我们抛开 @ 语法糖,一步步看看装饰器是如何工作的。

需求: 我们有一个 say_hello 函数,现在想在不修改它的情况下,在它执行前后各打印一条日志。

步骤 1:创建一个装饰器函数

这个函数接受一个函数(func)作为参数,并返回一个新的函数(wrapper)来“包装”或“装饰”原来的函数。

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 # 返回这个新函数

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

# 手动装饰:用 my_decorator 来“装饰” say_hello
# 相当于给 say_hello 穿上了 wrapper 这件“外衣”
decorated_function = my_decorator(say_hello)

# 调用被装饰后的新函数
decorated_function()

# 输出:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

步骤 2:使用语法糖 @

Python 提供了 @ 符号作为装饰器的语法糖,让代码更简洁、更易读。上面的手动装饰过程等价于:

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 # 这行代码等价于:say_hello = my_decorator(say_hello)
def say_hello():
    print("Hello!")

# 现在直接调用 say_hello,它已经是被装饰后的版本了
say_hello()
# 输出与上面完全相同

4. 处理带参数的函数:使用 *args 和 **kwargs

上面的装饰器很简单,但如果被装饰的函数 say_hello 有参数怎么办?wrapper 函数需要能够接受任何参数并将其传递给原始函数。这时就需要用到 *args 和 **kwargs

def my_decorator(func):
    def wrapper(*args, **kwargs): # 接受任意数量的位置参数和关键字参数
        print("Before the call.")
        # 将接收到的所有参数原封不动地传递给原始函数
        result = func(*args, **kwargs)
        print("After the call.")
        return result # 返回原始函数的执行结果
    return wrapper

@my_decorator
def greet(name, greeting="Hi"):
    print(f"{greeting}, {name}!")
    return "Done" # 让函数有一个返回值

# 调用带参数的被装饰函数
return_value = greet("Alice", greeting="Hello")
print(f"Function returned: {return_value}")

# 输出:
# Before the call.
# Hello, Alice!
# After the call.
# Function returned: Done

5. 解决身份问题:使用 functools.wraps

装饰器有一个“副作用”:它用新的函数(wrapper)替换了原始函数。这会导致原始函数的元信息(如名字 __name__、文档字符串 __doc__)丢失。

print(greet.__name__) # 输出:'wrapper',而不是 'greet'

为了解决这个问题,Python 的 functools 模块提供了一个有用的装饰器 wraps。它本身就是一个装饰器,用于装饰装饰器内部的 wrapper 函数,从而将原始函数的元信息复制过来。

最佳实践:始终使用 @functools.wraps

import functools

def my_decorator(func):
    @functools.wraps(func) # 这将保留 func 的元信息
    def wrapper(*args, **kwargs):
        print("Before the call.")
        result = func(*args, **kwargs)
        print("After the call.")
        return result
    return wrapper

@my_decorator
def greet(name):
    """This is a greeting function."""
    print(f"Hello, {name}!")

print(greet.__name__) # 输出:'greet'
print(greet.__doc__)  # 输出:'This is a greeting function.'

6. 带参数的装饰器(两层嵌套)

有时候你想让装饰器本身也能接受参数(例如 @repeat(4)),这就需要再嵌套一层函数。

import functools

def repeat(num_times): # 第一层:接受装饰器的参数
    def decorator_repeat(func): # 第二层:接受被装饰的函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs): # 第三层:包裹并执行原始函数
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result # 返回最后一次调用的结果
        return wrapper
    return decorator_repeat

# 使用带参数的装饰器
@repeat(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("World")

# 输出:
# Hello, World!
# Hello, World!
# Hello, World!

它的执行顺序是:@repeat(3) 先被调用,返回 decorator_repeat 函数。然后 @decorator_repeat 再被应用,就像普通的装饰器一样。


7. 装饰器的实际应用场景

装饰器非常实用,常见场景包括:

  • 日志记录(Logging):记录函数的调用信息和参数。
  • 权限校验(Authentication):在执行函数前检查用户是否登录或有足够权限。
  • 性能测试(Performance Timing):计算函数的运行时间。
  • 缓存(Memoization):存储昂贵函数调用的结果,避免重复计算(如 @functools.lru_cache)。
  • 注册函数(Registration):例如在 Web 框架中,用 @app.route(‘/’) 将函数注册为路由处理器。
  • 错误处理和重试(Error Handling & Retry):捕获异常并自动重试。

8. 类装饰器

除了函数,你也可以用类来构建装饰器。类通过实现 __call__ 方法变得可调用(callable),从而可以作为装饰器使用。

class CountCalls:
    def __init__(self, func):
        functools.wraps(self, func)(func) # 类似于 @wraps(func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)

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

say_hello() # 输出: Call 1 of 'say_hello' \n Hello!
say_hello() # 输出: Call 2 of 'say_hello' \n Hello!
print(say_hello.num_calls) # 输出: 2

总结

概念

描述

本质

一个接收函数作为参数、返回新函数的高阶函数。

语法

使用 @decorator_name 语法糖,使代码更简洁。

核心

依赖于 函数是一等公民 和 闭包

最佳实践

使用 *args, **kwargs 保证通用性,使用 @functools.wraps 保留元信息。

进阶

可以通过嵌套实现带参数的装饰器,也可以使用类实现类装饰器

用途

AOP(面向切面编程) 的完美实现,用于分离与核心业务逻辑无关的横切关注点(如日志、权限)。

希望这个从基础到进阶的解析能帮助你彻底理解 Python 装饰器!它是编写干净、优雅、可复用 Python 代码的利器。