1. 引言

在Python编程中,装饰器(Decorator)是一种非常强大且优雅的设计模式,它允许我们在不修改原有函数代码的情况下,为函数添加新的功能。装饰器本质上是一个可调用对象(函数或类),它接收一个函数作为参数,并返回一个新的函数。这种模式遵循了“开放封闭原则”——对扩展开放,对修改封闭。

无论是Web框架中的路由注册、Django中的权限控制、还是Flask中的请求预处理,装饰器都扮演着核心角色。掌握装饰器不仅能让你的代码更加简洁、可复用,更能帮助你深入理解Python的函数式编程特性。

本文将带你从最基础的闭包概念开始,逐步深入装饰器的底层原理,再通过大量实战案例,让你彻底掌握装饰器的编写与应用。

2. 装饰器的前置知识:闭包

要理解装饰器,必须先理解Python中的闭包(Closure)。闭包是指在一个外部函数中定义的内部函数,并且这个内部函数引用了外部函数的变量,同时外部函数将内部函数作为返回值返回。这样,即使外部函数执行完毕,内部函数依然可以记住它所引用的外部变量。

2.1 闭包的基本形式

def outer_func(msg):
    # 外部函数的局部变量
    message = msg
    
    def inner_func():
        # 内部函数引用了外部函数的变量
        print(message)
    
    return inner_func

# 创建闭包
my_closure = outer_func("Hello, Closure!")
# 调用内部函数
my_closure()  # 输出: Hello, Closure!

在这个例子中,`inner_func`就是一个闭包,它记住了外部函数`outer_func`中的`message`变量,即使`outer_func`已经执行完毕。

2.2 闭包的作用域特点

我们可以通过`__closure__`属性查看闭包中捕获的变量:

def outer(x):
    y = 10
    def inner():
        print(x, y)
    return inner

closure_func = outer(5)
print(closure_func.__closure__)  
# (<cell at 0x...: int object at 0x...>, <cell at 0x...: int object at 0x...>)
print(closure_func.__closure__[0].cell_contents)  # 5
print(closure_func.__closure__[1].cell_contents)  # 10

闭包的特性是实现装饰器的关键,因为装饰器本质上就是在内部函数中包裹被装饰函数,并访问外部函数传入的函数参数。

 2.3 闭包与普通嵌套函数的区别

- 普通嵌套函数:内部函数只在外部函数内部被调用,外部函数返回后无法再使用内部函数。
- 闭包:外部函数返回内部函数的引用,内部函数可以在外部函数结束后继续使用外部函数的变量。

 3. 装饰器的工作原理

从语法层面看,装饰器使用`@decorator`语法糖。但它的本质其实是高阶函数——接收函数作为参数并返回新函数的函数。下面这个例子揭示了装饰器的原始调用方式:

def simple_decorator(func):
    def wrapper():
        print("在函数执行前做点什么")
        func()
        print("在函数执行后做点什么")
    return wrapper

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

# 不使用@语法
say_hello = simple_decorator(say_hello)
say_hello()

输出:
```
在函数执行前做点什么
Hello!
在函数执行后做点什么
```而使用`@`语法糖等价于上面的手动赋值过程:

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

say_hello()  # 输出结果完全相同

所以装饰器并没有任何“魔法”,它就是一种函数替换:将原函数替换成装饰器返回的新函数(wrapper)。

4. 第一个装饰器:函数装饰器

 4.1 装饰无参数的函数

最简单的装饰器需要处理被装饰函数没有参数的情况:

def my_decorator(func):
    def wrapper():
        print("开始执行函数...")
        result = func()
        print("函数执行结束。")
        return result
    return wrapper

@my_decorator
def greet():
    print("欢迎来到Python世界!")

greet()
```

输出:
```
开始执行函数...
欢迎来到Python世界!
函数执行结束。
```

 4.2 装饰有参数的函数

如果被装饰函数带有参数,`wrapper`需要接受任意参数并传递给原函数。使用`*args`和`**kwargs`是最佳实践:

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")
        print(f"位置参数: {args}")
        print(f"关键字参数: {kwargs}")
        result = func(*args, **kwargs)
        print(f"返回值: {result}")
        return result
    return wrapper

@log_decorator
def add(a, b, c=0):
    return a + b + c

add(3, 5, c=2)
```

输出:
```
调用函数: add
位置参数: (3, 5)
关键字参数: {'c': 2}
返回值: 10
```

 4.3 装饰带有返回值的函数

注意`wrapper`必须返回原函数的返回值,否则装饰后的函数会变成`None`:

def preserve_return(func):
    def wrapper(*args, **kwargs):
        # 做一些额外操作
        result = func(*args, **kwargs)  # 保存返回值
        # 再做其他操作
        return result  # 必须返回
    return wrapper

@preserve_return
def multiply(x, y):
    return x * y

print(multiply(4, 5))  # 20

如果不写`return result`,则会输出`None`,造成意料之外的结果。

 5. 带参数的装饰器

有时候我们需要给装饰器传递参数,比如指定日志级别、重试次数、缓存过期时间等。这时需要三层嵌套函数:第一层接收装饰器参数,第二层接收被装饰函数,第三层接收被装饰函数的参数。

5.1 基本结构

def repeat(times):
    """重复执行被装饰函数times次"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for i in range(times):
                print(f"第{i+1}次执行")
                result = func(*args, **kwargs)
                results.append(result)
            return results
        return wrapper
    return decorator
@repeat(times=3)
def say_hello(name):
    return f"Hello, {name}!"

print(say_hello("Alice"))
```

输出:
```
第1次执行
第2次执行
第3次执行
['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
```

 5.2 带默认参数的装饰器

可以设计装饰器,让它既支持`@decorator`也支持`@decorator(arg)`两种用法(类似`lru_cache`的设计)。技巧是判断第一个参数是否为可调用对象:

from functools import wraps

def retry(max_attempts=3):
    if callable(max_attempts):
        # 如果直接@retry,max_attempts就是被装饰函数
        func = max_attempts
        max_attempts = 3
        return _retry_decorator(func, max_attempts)
    else:
        # 如果@retry(5),返回真正的装饰器
        def decorator(func):
            return _retry_decorator(func, max_attempts)
        return decorator

def _retry_decorator(func, max_attempts):
    @wraps(func)
    def wrapper(*args, **kwargs):
        for attempt in range(1, max_attempts + 1):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                print(f"第{attempt}次尝试失败: {e}")
                if attempt == max_attempts:
                    raise
        return None
    return wrapper

# 两种使用方式
@retry
def unstable_func1():
    import random
    if random.random() > 0.7:
        return "成功"
    raise ValueError("随机失败")

@retry(5)
def unstable_func2():
    # 同上
    pass

这种高级用法体现了装饰器的灵活性。

 6. 类装饰器

除了使用函数来实现装饰器,我们也可以使用类来实现。类装饰器通常利用`__init__`和`__call__`方法。类装饰器的优势在于可以保存状态,更适合需要维护配置信息的场景。

6.1 类作为装饰器

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"函数 {self.func.__name__} 已被调用 {self.count} 次")
        return self.func(*args, **kwargs)

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

say_hello()  # 第1次
say_hello()  # 第2次
say_hello()  # 第3次
```

输出:
```
函数 say_hello 已被调用 1 次
Hello!
函数 say_hello 已被调用 2 次
Hello!
函数 say_hello 已被调用 3 次
Hello!
```

6.2 带参数的类装饰器

如果需要给类装饰器传递参数,只需在`__init__`中接收参数,然后在`__call__`中接收函数:

class RepeatWithDelay:
    def __init__(self, times, delay=0):
        self.times = times
        self.delay = delay
    
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            import time
            results = []
            for i in range(self.times):
                results.append(func(*args, **kwargs))
                if i < self.times - 1:
                    time.sleep(self.delay)
            return results
        return wrapper

@RepeatWithDelay(times=3, delay=1)
def get_time():
    import time
    return time.time()

print(get_time())  # 每隔1秒打印一次时间戳

6.3 类装饰器与函数装饰器的选择

- 函数装饰器:简单、轻量,适合无状态或状态简单的场景。
- 类装饰器:适合需要维护复杂状态、提供多个方法(如`reset`计数)的场景,代码组织更清晰。

7. 多个装饰器的叠加与顺序

一个函数可以同时应用多个装饰器。执行顺序是从下往上(即离函数最近的装饰器先执行,但包装过程是从内到外)。看下面的例子:

def decorator_a(func):
    def wrapper(*args, **kwargs):
        print("A before")
        result = func(*args, **kwargs)
        print("A after")
        return result
    return wrapper

def decorator_b(func):
    def wrapper(*args, **kwargs):
        print("B before")
        result = func(*args, **kwargs)
        print("B after")
        return result
    return wrapper

@decorator_a
@decorator_b
def hello():
    print("Hello World")

hello()
```

输出:
```
A before
B before
Hello World
B after
A after
```

为什么?因为语法糖等价于:

hello = decorator_a(decorator_b(hello))

先执行`decorator_b(hello)`得到`wrapper_b`,再执行`decorator_a(wrapper_b)`得到`wrapper_a`。调用时先进入`wrapper_a`,打印"A before",然后调用内部的`wrapper_b`,打印"B before",再调用原始`hello`,打印"Hello World",然后依次退出。所以装饰器应用顺序是从下往上,执行顺序是从上往下进入,从下往上退出。

 8. `functools.wraps`的重要作用

装饰器有一个“副作用”:它会覆盖原函数的元信息(如`__name__`、`__doc__`、`__module__`等)。因为最终返回的是`wrapper`函数,原来的函数名等信息丢失了。这对于调试、序列化、文档生成都不友好。

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        """这是wrapper的文档"""
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def add(a, b):
    """计算两个数的和"""
    return a + b

print(add.__name__)   # wrapper
print(add.__doc__)    # 这是wrapper的文档

为了解决这个问题,Python的`functools`模块提供了`wraps`装饰器,它可以将被装饰函数的元信息拷贝到`wrapper`函数上。

from functools import wraps

def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """这是wrapper的文档,但会被覆盖"""
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def add(a, b):
    """计算两个数的和"""
    return a + b

print(add.__name__)   # add
print(add.__doc__)    # 计算两个数的和

强烈建议:在编写任何装饰器时,务必在`wrapper`上使用`@wraps(func)`。这是Python最佳实践。

 9. 实战应用场景

下面通过几个真实场景展示装饰器的威力。

9.1 函数执行时间统计

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        elapsed = (end - start) * 1000
        print(f"{func.__name__} 执行耗时: {elapsed:.4f} 毫秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.5)
    return "完成"

slow_function()  # 输出: slow_function 执行耗时: 500.1234 毫秒

可以扩展为记录日志到文件,或者只统计超过阈值的调用。

 9.2 权限校验装饰器(模拟Flask-Login)

from functools import wraps
from flask import session, abort

def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if 'user_id' not in session:
            abort(401)  # 未授权
        return func(*args, **kwargs)
    return wrapper

# 带角色校验的增强版
def role_required(allowed_roles):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            user_role = session.get('role')
            if user_role not in allowed_roles:
                abort(403)  # 禁止访问
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 使用示例
@app.route('/admin')
@login_required
@role_required(['admin'])
def admin_panel():
    return "欢迎管理员"

 9.3 重试机制装饰器

import time
from functools import wraps

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"第{attempt}次尝试失败: {e}")
                    if attempt == max_attempts:
                        raise
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=5, delay=2, exceptions=(ConnectionError, TimeoutError))
def fetch_data():
    import random
    if random.random() < 0.8:
        raise ConnectionError("网络连接失败")
    return "数据内容"

 9.4 缓存装饰器(简单LRU)

Python内置的`functools.lru_cache`就是最好的例子,我们也可以自己实现一个简单的版本:

from functools import wraps

def simple_cache(func):
    cache = {}
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 为了简化,将参数转为tuple作为key(不处理kwargs中的可变对象)
        key = args + tuple(sorted(kwargs.items()))
        if key not in cache:
            print(f"计算新结果: {func.__name__}{args}")
            cache[key] = func(*args, **kwargs)
        else:
            print(f"从缓存读取: {func.__name__}{args}")
        return cache[key]
    return wrapper

@simple_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # 中间结果会被缓存,加速递归计算

 9.5 日志记录装饰器

import logging
from functools import wraps

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def log(level=logging.INFO):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            logging.log(level, f"调用 {func.__name__} 参数: args={args}, kwargs={kwargs}")
            try:
                result = func(*args, **kwargs)
                logging.log(level, f"{func.__name__} 返回: {result}")
                return result
            except Exception as e:
                logging.exception(f"{func.__name__} 异常: {e}")
                raise
        return wrapper
    return decorator

@log(level=logging.INFO)
def divide(a, b):
    return a / b

divide(10, 2)
divide(10, 0)  # 会记录异常

 9.6 单例模式装饰器

使用类装饰器实现单例模式:

def singleton(cls):
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self):
        print("初始化数据库连接(仅一次)")
        self.connected = True

db1 = DatabaseConnection()  # 输出初始化信息
db2 = DatabaseConnection()  # 无输出,返回同一个实例
print(db1 is db2)  # True

10. 总结与最佳实践

10.1 装饰器核心要点回顾

1. 本质:装饰器是接收函数并返回新函数的高阶函数。
2. 闭包:依赖闭包来“记忆”被装饰函数。
3. 参数处理:使用`*args, **kwargs`确保通用性。
4. 返回值:wrapper必须返回原函数的返回值。
5. 元信息:始终使用`functools.wraps`保留原函数元数据。
6. 嵌套层级:
   - 无参数装饰器:2层(装饰器函数 + wrapper)
   - 带参数装饰器:3层(参数接收层 + 装饰器层 + wrapper)

10.2 常见错误与陷阱

- 忘记返回wrapper:导致装饰后函数变成`None`。
- 不保留`args, kwargs`:无法装饰带参数的函数。
- 忽略返回值:调用func后忘记return。
- 多个装饰器的顺序错误:例如在`@login_required`之后使用`@timer`,可能导致计时包含重定向逻辑。
- 修改可变默认参数:装饰器中的列表/字典作为默认参数会被多次调用共享。

10.3 何时使用装饰器?

- 横切关注点(Aspect-Oriented Programming):日志、性能计时、事务管理、权限校验、缓存、重试。
- 函数注册:如Flask的路由注册、插件系统。
- 输入验证/类型检查:在函数执行前校验参数类型。
- 同步/异步转换:将同步函数转为异步执行(需配合线程池)。

装饰器是Python中一道分水岭:理解装饰器意味着你从“会用Python”进阶到“理解Python的设计哲学”。希望本文的详细讲解和大量代码示例能够帮助你彻底掌握装饰器。在未来的开发中,善用装饰器可以让你的代码更加优雅、可维护。

更多推荐