38. 装饰器:为函数添加行为
装饰器(decorator)是 Python 中用于编写整洁、可复用代码的最强大特性之一。它们允许你在不更改函数实际代码的情况下,修改或增强函数的行为。在本章中,我们将在第 23 章对一等函数(first-class function)与闭包(closure)的理解基础上,进一步探索装饰器的工作原理以及如何有效使用它们。
38.1) 装饰器是什么,以及为什么它们很有用
装饰器(decorator) 是一个函数,它以另一个函数作为输入,并返回该函数的一个修改版本。这之所以可行,是因为正如我们在第 23 章学到的,Python 中的函数是“一等对象(first-class object)”——它们可以作为参数传递,并从其他函数中返回。装饰器让你能够在现有函数周围“包裹(wrap)”额外的行为,从而轻松添加诸如日志记录(logging)、计时(timing)、校验(validation)或访问控制(access control)等通用功能,而不会让核心逻辑变得杂乱。
为什么装饰器很重要
想象一下,你的程序里有多个函数,并且你想记录每个函数何时被调用。如果不使用装饰器,你可能会写出这样的代码:
# 不使用装饰器 - 重复的日志记录代码
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# 使用这些函数
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60这种做法有几个问题:
- 代码重复:每个函数里都重复了日志记录的代码行
- 关注点混杂:日志记录代码与业务逻辑混在一起
- 难以维护:如果你想更改日志格式,就必须更新每个函数
- 容易遗漏:新函数可能不会包含日志记录
装饰器通过让你把日志行为与核心函数分离来解决这些问题:
# 使用装饰器 - 干净且易维护
# (我们会在本章学习如何创建 @log_calls)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# 使用这些函数会产生相同的输出
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60区别在哪里? 日志行为只在 @log_calls 装饰器中定义一次,然后在所有地方复用。你的核心函数保持干净,并专注于其主要目的。
装饰器的常见使用场景
装饰器尤其适用于:
- 日志记录:记录函数何时被调用以及它们返回什么
- 计时:测量函数执行所需的时间
- 校验:检查函数参数是否满足特定要求
- 缓存:存储代价高昂的函数调用结果以便复用
- 访问控制:在允许函数执行之前检查权限
- 重试逻辑:自动重试失败的操作
- 类型检查:校验参数与返回值类型
关键优势在于:你只需编写一次装饰器,就能用一行代码把它应用到许多函数上。
38.2) 函数作为对象:装饰器的基础
在我们理解装饰器之前,需要回顾并扩展“函数在 Python 中是一等对象(first-class object)”这一概念。正如我们在第 23 章学到的,这意味着函数可以赋值给变量、作为参数传递,以及从其他函数中返回。
函数可以赋值给变量
当你定义一个函数时,Python 会创建一个函数对象,并将其绑定到一个名称上:
def greet(name):
return f"Hello, {name}!"
# 函数对象可以赋值给另一个变量
say_hello = greet
# 两个名字都指向同一个函数对象
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!名称 greet 和 say_hello 都指向同一个函数对象。这是装饰器工作方式的基础。
函数可以作为参数传递
你可以像传递其他值一样,把函数传给其他函数:
def apply_twice(func, value):
"""对一个值应用某个函数两次。"""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)这里,apply_twice 接收 add_five 函数作为参数,并调用它两次。
函数可以返回其他函数
一个函数可以创建并返回一个新函数:
def make_multiplier(factor):
"""创建一个按指定因子进行乘法的函数。"""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50make_multiplier 函数返回一个新函数,该函数通过闭包(closure)“记住”了 factor 的值(正如我们在第 23 章学到的)。
包裹函数:核心装饰器模式
装饰器模式结合了这些概念:一个函数接收另一个函数作为输入,创建一个增加行为的包装函数(wrapper function),并返回这个包装函数:
def simple_wrapper(original_func):
"""为一个函数包裹额外的行为。"""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# 手动包裹函数
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greeting让我们跟踪一下发生了什么:
simple_wrapper接收say_hello作为original_func- 它创建一个新函数
wrapper,该函数会:- 打印 "Before calling the function"
- 调用
original_func()(也就是say_hello) - 打印 "After calling the function"
- 返回结果
simple_wrapper返回wrapper函数- 当我们调用
wrapped_hello()时,实际上是在调用wrapper,而它会在内部调用原始的say_hello
这就是所有装饰器背后的核心模式。
处理带参数的函数
上面的包装器只适用于不带参数的函数。要让它适用于任何函数,我们需要 *args 和 **kwargs:
def flexible_wrapper(original_func):
"""包裹一个可以接收任意参数的函数。"""
def wrapper(*args, **kwargs):
# *args 捕获位置参数
# **kwargs 捕获关键字参数
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# 手动包裹函数
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!*args 和 **kwargs 是如何工作的:
正如我们在第 20 章学到的,*args 和 **kwargs 允许函数接收可变数量的参数:
*args将所有位置参数收集到一个元组(tuple)中**kwargs将所有关键字参数收集到一个字典(dictionary)中- 当我们调用
original_func(*args, **kwargs)时,会把它们再解包成原函数的参数
这种模式让我们的包装器能够适用于任何函数,而不管它需要多少参数。
迁移到更简洁的语法
这种模式就是装饰器的基础。我们接下来要学习的装饰器语法,只是应用该模式的一种更简洁的方式。我们不再写:
greet = flexible_wrapper(greet)而是使用 @ 语法:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"两者做的事情完全相同——@ 语法只是语法糖(syntactic sugar),让代码更简洁、更易读。
38.3) @decorator 语法:更简洁的应用方式
写 function_name = decorator(function_name) 虽然可行,但很啰嗦也容易忘记。Python 提供了 @decorator 语法,用于更干净地应用装饰器。
使用 @ 符号
与手动包裹函数不同,你可以把 @decorator_name 放在函数定义的上一行:
def log_call(func):
"""记录函数调用的装饰器。"""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# 使用被装饰的函数
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0@log_call 语法与下面写法完全等价:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)但 @ 语法更简洁,并且能让人立刻看出该函数被装饰了。
堆叠多个装饰器
你可以通过堆叠(stacking)把多个装饰器应用到同一个函数上:
import time
def log_call(func):
"""记录函数调用的装饰器。"""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""为函数执行计时的装饰器。"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30当装饰器堆叠时,它们会按照 从下到上(离函数更近的先应用)的顺序被应用:
@timer # 第二个应用(最外层)
@log_call # 第一个应用(最靠近函数)
def process_data(items):
pass这等价于:
process_data = timer(log_call(process_data))应用顺序(从下到上):
@log_call先包裹原始函数@timer再包裹结果(包裹已经被包裹过的函数)
执行顺序(从上到下,从最外层到最内层):
timer的 wrapper 开始(最外层,先执行)log_call的 wrapper 开始(内层 wrapper)- 原始函数执行
log_call的 wrapper 结束timer的 wrapper 结束(最外层,最后结束)
把装饰器想成一层层的包装纸:你是从里往外包的,但在拆开(执行)时,是从外往里拆。
装饰器应用:
执行流程:
38.4) 实用的装饰器示例(日志、计时、校验)
现在让我们探索几个你在真实程序中可能会用到的实用装饰器。这些示例展示了常见模式,并说明装饰器如何解决现实世界的问题。
示例 1:增强的日志装饰器
一个更复杂的日志装饰器,包含时间戳并处理异常:
import time
def log_with_timestamp(func):
"""记录带时间戳的函数调用的装饰器。"""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# 模拟处理过程
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# 测试成功执行
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# 测试带校验的成功执行
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# 测试异常处理
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user ID这个装饰器:
- 为所有日志消息添加时间戳
- 记录成功完成与异常两种情况
- 在记录日志后重新抛出异常(使用不带参数的
raise) - 使用
try/except块捕获并记录任何异常
示例 2:性能计时装饰器
一个测量并报告函数执行时间的装饰器:
import time
def measure_time(func):
"""测量并报告执行时间的装饰器。"""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# 以合适的方式格式化时间
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""找出所有不超过 limit 的素数。"""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""计算 n 的阶乘。"""
result = 1
for i in range(1, n + 1):
result *= i
return result
# 测试被装饰的函数
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits这个装饰器会根据持续时间自动选择合适的时间单位(微秒、毫秒或秒)来格式化计时结果。
示例 3:输入校验装饰器
一个在执行前校验函数参数的装饰器:
def validate_positive(func):
"""确保所有数值参数为正数的装饰器。"""
def wrapper(*args, **kwargs):
# 检查位置参数
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# 检查关键字参数
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""计算矩形面积。"""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""计算折扣后的价格。"""
discount = price * (discount_percent / 100)
return price - discount
# 测试有效输入
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# 测试无效输入
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10这个装饰器:
- 检查所有数值参数(位置参数与关键字参数)
- 如果发现任何非正数参数,就抛出描述清晰的错误
- 提供明确的错误信息,指出哪个参数未通过校验
38.5)(可选)带参数的装饰器
到目前为止,我们的所有装饰器都是简单的函数:接收一个函数作为输入。但如果你想配置装饰器的行为怎么办?例如,你可能想要一个重试装饰器,可以指定尝试次数;或者一个日志装饰器,可以指定日志级别。
带参数的装饰器 需要额外一层函数嵌套。装饰器不再是“接收函数的函数”,而变成“接收参数并返回一个装饰器的函数”。
模式:装饰器工厂
带参数的装饰器实际上是一个 装饰器工厂(decorator factory)——一个创建并返回装饰器的函数。理解这一点的关键是知道 Python 对 @ 符号做了什么。
核心原则:Python 会先求值 @
Python 总是先对 @ 后面的内容进行求值,然后使用其结果来装饰你的函数。
让我们对比一下:
A) 基础装饰器:
基于这个示例:
def log_call(func):
"""记录函数调用的装饰器。"""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"Python 所做的事情:
- 对
@log_call求值 → 结果:log_call本身(函数对象) - 应用到
greet:greet = log_call(greet)
B) 装饰器工厂:
基于这个示例:
def repeat(times):
"""第 1 层:工厂 - 接收配置"""
def decorator(func):
"""第 2 层:装饰器 - 接收要装饰的函数"""
def wrapper(*args, **kwargs):
"""第 3 层:包装器 - 在被装饰函数被调用时执行"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Python 所做的事情:
- 对
@repeat(3)求值 → 结果:调用repeat(3),并返回一个装饰器函数 - 把该装饰器应用到
greet:greet = decorator(greet)
区别在于:@log_call 给你的是函数本身,但 @repeat(3) 会调用一个函数(repeat),该函数返回一个装饰器。
理解三层结构
装饰器工厂有三个嵌套函数,每一层都有特定角色:
def repeat(times): # 第 1 层:工厂
def decorator(func): # 第 2 层:装饰器
def wrapper(*args, **kwargs): # 第 3 层:包装器
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator第 1 层 - 工厂(repeat):
- 接收:配置(
times) - 返回:一个装饰器函数
- 调用时机:Python 对
@repeat(3)求值时
第 2 层 - 装饰器(decorator):
- 接收:要装饰的函数(
func) - 返回:一个包装函数
- 调用时机:紧接第 1 层之后,作为 @ 语法的一部分
第 3 层 - 包装器(wrapper):
- 接收:函数被调用时传入的参数(
*args, **kwargs) - 返回:结果
- 调用时机:每次调用被装饰函数时
逐步执行过程
让我们跟踪 @repeat(3) 发生了什么:
# 你写的是:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")第 1 步: Python 对 repeat(3) 求值
decorator = repeat(3) # 工厂返回一个装饰器(捕获 times=3)第 2 步: Python 把装饰器应用到 greet
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # 装饰器返回一个 wrapper(捕获 func=greet)注意: 此时,greet 现在指向 wrapper 函数。原始的 greet 被捕获在 func 中。
第 3 步: 当你调用 greet("Alice") 时,wrapper 执行
greet("Alice") # 实际是在调用 wrapper("Alice")
# wrapper 使用捕获的 'times' 和 'func'为什么需要三层?
每一层通过闭包(closure)捕获不同的信息:
def repeat(times): # 捕获:times
def decorator(func): # 捕获:func(并记住 times)
def wrapper(*args, **kwargs): # 捕获:times、func,并接收 args
for i in range(times): # 使用捕获的 'times'
result = func(*args, **kwargs) # 使用捕获的 'func' 与 'args'
return result
return wrapper
return decorator- 第 1 层 捕获配置(
times) - 第 2 层 捕获要装饰的函数(
func) - 第 3 层 接收调用时的参数(
args、kwargs)
如果没有这三层,我们就无法拥有一个可配置的装饰器,它既能记住自身设置,也能记住它所装饰的函数。
示例 1:可配置的日志装饰器
下面是一个接受配置的日志装饰器的实用示例:
def log_with_prefix(prefix="LOG"):
"""装饰器工厂:创建带自定义前缀的日志装饰器。"""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # 使用默认前缀
def get_average(numbers):
return sum(numbers) / len(numbers)
# 测试被装饰的函数
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0注意:
@log_with_prefix(prefix="INFO")使用自定义前缀@log_with_prefix()使用默认前缀 "LOG"- 即便使用默认值,你也必须写上括号
示例 2:带多个参数的装饰器
下面是一个校验数值范围的装饰器:
def validate_range(min_value=None, max_value=None):
"""
装饰器工厂:校验数值参数是否在某个范围内。
Args:
min_value: 允许的最小值(包含)
max_value: 允许的最大值(包含)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# 检查所有数值参数
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""计算百分比。"""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""根据出生年份计算年龄。"""
return current_year - birth_year
# 测试有效输入
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# 测试无效输入
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0何时使用带参数的装饰器
在以下情况使用带参数的装饰器:
- 你需要配置装饰器的行为
- 同一个装饰器在不同上下文中应该表现不同
- 你希望让装饰器更可复用、更灵活
常见示例包括:
- 可配置尝试次数与延迟的重试装饰器
- 可配置日志级别或格式的日志装饰器
- 可配置规则的校验装饰器
- 可配置缓存大小或过期时间的缓存装饰器
- 可配置限制值的限流装饰器
关于复杂度的说明
带参数的装饰器会增加一层复杂度。编写它们时:
- 使用清晰、描述性的参数名
- 提供合理的默认值
- 包含解释参数的文档字符串(docstring)
- 考虑额外的灵活性是否值得增加的复杂度
对于简单场景,不带参数的装饰器通常更清晰,也更容易理解。
装饰器是编写整洁、可维护 Python 代码的强大工具。它们让你能够把横切关注点(cross-cutting concern)(例如日志记录、计时与校验)从核心业务逻辑中分离出来,使代码更易阅读、测试与修改。随着你继续使用 Python 编程,你会发现装饰器在框架与库中被大量使用,并且你也会发现很多机会,去编写自己的装饰器,以优雅的方式解决常见问题。