1. 开篇:为什么需要装饰器?

装饰器的核心思想是在不修改被装饰对象(通常是函数或方法)的源代码和调用方式的前提下,为其增加额外的功能

一个简单的问题场景

假设我们有一个核心业务函数:

1
2
def business_logic():
print("执行核心业务逻辑...")

现在,我们有一个新的需求:在每次执行这个函数前后,打印一条日志,记录函数的开始和结束。

不使用装饰器的解决方案

方案一:直接修改函数代码(最差的方式)

1
2
3
4
def business_logic():
print("开始执行函数...")
print("执行核心业务逻辑...")
print("函数执行结束。")
  • 弊端
    • 违反了开放/封闭原则:我们修改了函数的内部实现。
    • 代码冗余:如果有很多函数都需要这个日志功能,我们就得在每个函数里重复添加这些代码,违反了 DRY (Don’t Repeat Yourself) 原则。

方案二:定义一个新的函数来包装(接近装饰器的思想)

1
2
3
4
5
6
7
def logged_business_logic():
print("开始执行函数...")
business_logic()
print("函数执行结束。")

# 调用时需要用新的函数名
logged_business_logic()
  • 弊端
    • 我们必须使用一个新的函数名 (logged_business_logic),所有原来调用 business_logic 的地方都需要修改,这在大型项目中是不可接受的。

我们需要一种方法,既能添加功能,又不改变 business_logic 的源码和调用方式。这就引出了装饰器的核心思想。

2. 核心前置知识:Python 函数是一等公民

在 Python 中,函数是“一等公民”(First-Class Citizens),这意味着它们可以像任何其他对象(如整数、字符串、列表)一样被对待。

a. 函数可以被赋值给变量

1
2
3
4
5
def say_hello():
print("Hello!")

greet = say_hello # 将函数对象赋值给变量 greet
greet() # 通过 greet 调用函数,输出 "Hello!"

b. 函数可以作为参数传递

1
2
3
4
5
6
7
8
9
10
11
12
def process_data(data, operation_func):
print("开始处理数据...")
result = operation_func(data)
print(f"处理结果: {result}")

def square(n):
return n * n

process_data(5, square) # 将 square 函数作为参数传递
# 输出:
# 开始处理数据...
# 处理结果: 25

c. 函数可以作为返回值(闭包)

这是理解装饰器最关键的一点。一个函数可以定义在另一个函数内部,并且内部函数可以引用外部函数的变量。当外部函数返回内部函数时,就形成了一个闭包(Closure)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def outer_function(msg):
message = msg

def inner_function():
# inner_function 捕获了外部作用域的变量 message
print(message)

return inner_function # 返回的是内部函数的对象,而不是调用结果

hello_func = outer_function("Hello")
goodbye_func = outer_function("Goodbye")

hello_func() # 输出: Hello
goodbye_func() # 输出: Goodbye

hello_funcgoodbye_func 就是闭包,它们“记住”了创建它们时所处的环境(即 message 变量的值)。

3. 装饰器的诞生:从手动装饰到语法糖

有了上面的知识,我们现在可以完美地解决第 1 节中的问题了。

手动实现装饰器逻辑

我们可以编写一个函数,它接收一个函数作为参数,并返回一个“增强版”的新函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def logger_decorator(original_function):
def wrapper():
print(f"开始执行函数: {original_function.__name__}")
original_function() # 调用原始函数
print(f"函数 {original_function.__name__} 执行结束。")
return wrapper # 返回包装后的新函数

def business_logic():
print("执行核心业务逻辑...")

# 这就是“装饰”的过程
decorated_business_logic = logger_decorator(business_logic)
decorated_business_logic()

# 输出:
# 开始执行函数: business_logic
# 执行核心业务逻辑...
# 函数 business_logic 执行结束。

我们成功了!但是,我们还是得调用一个新的函数名 decorated_business_logic。不过,我们可以利用“函数可以被赋值给变量”的特性:

1
2
business_logic = logger_decorator(business_logic)
business_logic() # 现在可以直接调用原函数名,但它实际上已经是包装后的函数了

这行 business_logic = logger_decorator(business_logic) 就是装饰器工作的核心原理

@ 语法糖:让代码更优雅

Python 提供了一个专门的语法糖 @ 来简化这个赋值过程。

1
2
3
4
5
6
@logger_decorator
def business_logic():
"""这是一个处理核心业务的函数。"""
print("执行核心业务逻辑...")

business_logic()

上面这段代码和下面这句是完全等价的

1
2
3
4
5
def business_logic():
"""这是一个处理核心业务的函数。"""
print("执行核心业务逻辑...")

business_logic = logger_decorator(business_logic)

@decorator 放在函数定义前,就表示在函数定义之后,立即将这个函数作为参数传递给 decorator,并将返回值重新赋给原函数名。

4. 构建一个通用的装饰器

我们上面的 logger_decorator 太简单了,它不能处理带参数和返回值的函数。

1
2
3
4
5
@logger_decorator
def greet(name, message="Hello"):
return f"{message}, {name}!"

# greet("Alice") # 这会报错! TypeError: wrapper() takes 0 positional arguments but 1 was given

a. 处理被装饰函数的参数 (*args, **kwargs)

为了让我们的装饰器能够接受任意参数,我们需要在 wrapper 函数中使用 *args**kwargs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def general_decorator(func):
def wrapper(*args, **kwargs):
print(f"函数 {func.__name__} 即将执行,参数: args={args}, kwargs={kwargs}")
# 将捕获的参数原封不动地传递给原始函数
func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕。")
return wrapper

@general_decorator
def greet(name, message="Hello"):
print(f"{message}, {name}!")

greet("Alice")
greet("Bob", message="Hi")

b. 处理被装饰函数的返回值

如果被装饰函数有返回值,我们的 wrapper 也应该将它返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def general_decorator_with_return(func):
def wrapper(*args, **kwargs):
print(f"函数 {func.__name__} 即将执行...")
result = func(*args, **kwargs) # 调用原始函数并保存返回值
print(f"函数 {func.__name__} 执行完毕,返回值为: {result}")
return result # 将返回值传递出去
return wrapper

@general_decorator_with_return
def add(a, b):
return a + b

sum_result = add(10, 5)
print(f"最终结果: {sum_result}")
# 输出:
# 函数 add 即将执行...
# 函数 add 执行完毕,返回值为: 15
# 最终结果: 15

5. functools.wraps:一个不可或缺的助手

到目前为止,我们的装饰器看起来很完美,但其实有一个隐藏的问题:它丢失了原函数的元信息(metadata),比如函数名、文档字符串(docstring)等。

未使用 wraps 的问题

1
2
3
4
5
6
7
8
# 使用上面的 general_decorator_with_return
@general_decorator_with_return
def business_logic():
"""这是一个处理核心业务的函数。"""
print("执行核心业务逻辑...")

print(business_logic.__name__) # 输出: wrapper (应该是 business_logic)
print(business_logic.__doc__) # 输出: None (应该是 "这是一个...")

这是因为 business_logic 这个名字现在指向的是 wrapper 函数,它的元信息自然是 wrapper 的。这对于调试、文档生成和代码自省非常不利。

@wraps 的作用和用法

functools 模块中的 wraps 本身也是一个装饰器,专门用来装饰我们的 wrapper 函数,它能将原始函数的元信息复制到 wrapper 函数上。

最终、最规范的装饰器模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import wraps

def perfect_decorator(func):
@wraps(func) # 核心!将 func 的元信息复制到 wrapper
def wrapper(*args, **kwargs):
"""这是 wrapper 函数的文档字符串。"""
# 前置操作
print(f"执行函数 '{func.__name__}' 之前...")

result = func(*args, **kwargs)

# 后置操作
print(f"执行函数 '{func.__name__}' 之后...")
return result
return wrapper

@perfect_decorator
def business_logic():
"""这是一个处理核心业务的函数。"""
print("执行核心业务逻辑...")

print(business_logic.__name__) # 输出: business_logic (正确!)
print(business_logic.__doc__) # 输出: 这是一个处理核心业务的函数。 (正确!)
help(business_logic) # 也会显示 business_logic 的帮助信息

记住:编写任何装饰器时,都应该使用 @functools.wraps

6. 进阶装饰器

a. 带参数的装饰器

如果我们想让装饰器本身可以接收参数,比如 @repeat(3),让函数执行 3 次。

这就需要一个三层嵌套的结构:一个函数(工厂函数)接收参数,返回一个装饰器,这个装饰器再返回 wrapper 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import wraps

def repeat(num_times):
# 1. 这是一个装饰器工厂,接收参数,返回一个真正的装饰器
def decorator_repeat(func):
# 2. 这是我们熟悉的装饰器
@wraps(func)
def wrapper(*args, **kwargs):
# 3. 这是包装函数
total_result = None
for _ in range(num_times): # 使用来自工厂的参数 num_times
total_result = func(*args, **kwargs)
return total_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!

执行过程分解

  1. repeat(num_times=3) 被首先调用。
  2. 它返回 decorator_repeat 函数。
  3. Python 接着执行 @decorator_repeat,即 greet = decorator_repeat(greet)
  4. decorator_repeat 返回 wrapper 函数,并赋值给 greet

所以,@repeat(3) 实际上是 @(repeat(3)) 的效果。

b. 类装饰器

我们也可以用类来实现装饰器。这在需要维护状态时特别有用。一个类要成为装饰器,需要实现 __init____call__ 方法。

  • __init__:接收被装饰的函数。
  • __call__:实现 wrapper 的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CallCounter:
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # 更新类的元信息,让其看起来像被包装的函数

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

@CallCounter
def say_whee():
print("Whee!")

say_whee() # 输出: 函数 'say_whee' 已被调用 1 次。 Whee!
say_whee() # 输出: 函数 'say_whee' 已被调用 2 次。 Whee!
say_whee() # 输出: 函数 'say_whee' 已被调用 3 次。 Whee!

c. 装饰器栈(多个装饰器)

一个函数可以被多个装饰器同时装饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import wraps

def decorator_1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("进入 Decorator 1")
result = func(*args, **kwargs)
print("离开 Decorator 1")
return result
return wrapper

def decorator_2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("进入 Decorator 2")
result = func(*args, **kwargs)
print("离开 Decorator 2")
return result
return wrapper

@decorator_1
@decorator_2
def my_function():
print("执行 my_function")

执行顺序
装饰器的应用顺序是从下到上(靠近函数的先应用)。
所以 my_function 首先被 decorator_2 包装,然后这个结果再被 decorator_1 包装。
my_function = decorator_1(decorator_2(my_function))

而代码的执行顺序则是像洋葱一样,从外到内,再从内到外。

1
2
3
4
5
6
7
my_function()
# 输出:
# 进入 Decorator 1
# 进入 Decorator 2
# 执行 my_function
# 离开 Decorator 2
# 离开 Decorator 1

7. 实际应用场景

  • 日志记录 (Logging):在函数执行前后记录日志信息,如我们最初的例子。
  • 性能计时 (Timing):记录函数执行所需的时间。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import time
    def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
    start_time = time.perf_counter()
    result = func(*args, **kwargs)
    end_time = time.perf_counter()
    print(f"'{func.__name__}' took {end_time - start_time:.4f} seconds.")
    return result
    return wrapper
  • 权限校验 (Authorization):在 Web 框架(如 Flask, Django)中,检查用户是否登录或有特定权限。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Flask 示例
    def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
    if g.user is None:
    return redirect(url_for('login', next=request.url))
    return f(*args, **kwargs)
    return decorated_function

    @app.route('/secret')
    @login_required
    def secret_page():
    return "这是秘密页面!"
  • 缓存 (Caching / Memoization):对于计算成本高的函数,缓存其结果。Python 3.9+ 提供了 functools.cache
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import functools
    import time

    @functools.cache
    def fibonacci(n):
    if n < 2:
    return n
    time.sleep(0.1) # 模拟耗时计算
    return fibonacci(n-1) + fibonacci(n-2)

    # 第一次调用会很慢
    print(fibonacci(10))
    # 第二次调用会立刻返回结果,因为结果被缓存了
    print(fibonacci(10))
  • 输入验证 (Input Validation):在函数执行前检查输入参数是否合法。

8. 总结

  • 核心思想:装饰器是一个函数,它接收一个函数作为输入,并返回一个新的函数,旨在不修改原函数代码的情况下为其添加功能。
  • 语法糖@decoratormy_func = decorator(my_func) 的简写。
  • 基础模板:装饰器内部通常定义一个 wrapper 函数,使用 *args, **kwargs 来处理任意参数,并确保返回原函数的计算结果。
  • 最佳实践务必使用 @functools.wraps 来保留原函数的元信息。
  • 灵活性:装饰器可以通过带参数、使用类、或层叠使用来满足复杂的需求。
  • 强大功能:它们是 Python 中实现横切关注点(如日志、权限、缓存)的强大工具,让业务代码保持纯净和专注。