Python 编程:从零基础到精通
第 34 课:装饰器基础(函数包装)
所属模块: 模块八:装饰器与高级主题
难度: Advanced
标签: decorators, wrappers
学习目标
完成本课学习后,你将能够:
- 理解装饰器的核心思想——"函数包装"或"函数增强"。
- 掌握如何从零开始编写一个简单的装饰器。
- 运用装饰器为函数添加通用的功能,例如日志记录和性能测量。
- 熟练使用 Python 的
@语法糖来应用装饰器。
核心概念
1. 什么是装饰器?
想象一下,你有一件基础款T恤(一个基础函数),但它有些单调。你给它添加了一个口袋(添加了新功能),这个过程就是"装饰"。在Python中,装饰器就是一个"函数工厂",它接收一个函数作为输入,然后返回一个被"增强"后的新函数。
装饰器本质上是一个函数,它让你可以在不修改原函数代码的情况下,给原函数增加额外的功能。
2. 装饰器的工作原理(手动包装)
在没有 @ 语法糖之前,我们可以通过手动包装来实现类似效果。关键在于高阶函数:函数可以作为参数传递,也可以作为返回值返回。
def say_hello():
print("Hello!")
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 # 返回包装器函数,而不是调用它
# 手动"装饰"函数
wrapped_say_hello = my_decorator(say_hello)
wrapped_say_hello()
输出:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
这里,my_decorator 就是一个装饰器。它"包装"了 say_hello 函数,创建了一个新的函数 wrapper,这个新函数在调用原函数前后都执行了额外代码。
3. 使用 @ 语法糖
Python 提供了 @ 语法,让上面的手动过程变得简洁优雅。下面两种写法是完全等价的:
# 写法一:使用@语法糖
@my_decorator
def say_hello():
print("Hello!")
# 等价于 写法二:手动包装
say_hello = my_decorator(say_hello)
使用 @ 后,代码更清晰,意图更明确。
代码示例
示例1:一个简单的日志装饰器
这个装饰器会在每次调用函数时,打印出函数的名称和调用时间。
import functools
from datetime import datetime
def log_calls(func):
"""一个记录函数调用的装饰器"""
# @functools.wraps(func) 是一个很重要的工具,它能保留原函数的元信息(如__name__, __doc__)
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""这是包装器函数"""
# 在函数被调用前记录信息
print(f"[{datetime.now()}] Calling function '{func.__name__}'...")
# 调用原始函数
result = func(*args, **kwargs)
# 在函数被调用后记录信息
print(f"[{datetime.now()}] Function '{func.__name__}' finished.")
return result
return wrapper
# 使用@语法应用装饰器
@log_calls
def add(a, b):
"""返回两个数的和"""
print(f"Computing {a} + {b}")
return a + b
@log_calls
def greet(name):
"""向某人问好"""
print(f"Hello, {name}!")
# 调用被装饰的函数
print("Test 1: add function")
result = add(3, 5)
print(f"Result: {result}\n")
print("Test 2: greet function")
greet("Alice")
print()
# 查看函数的元信息是否被保留
print(f"add function name: {add.__name__}")
print(f"add function docstring: {add.__doc__}")
输出示例(时间戳会变化):
Test 1: add function
[2023-10-27 10:00:00.123456] Calling function 'add'...
Computing 3 + 5
[2023-10-27 10:00:00.123456] Function 'add' finished.
Result: 8
Test 2: greet function
[2023-10-27 10:00:00.123456] Calling function 'greet'...
Hello, Alice!
[2023-10-27 10:00:00.123456] Function 'greet' finished.
add function name: add
add function docstring: 返回两个数的和
示例2:一个计时装饰器
这个装饰器用于测量函数执行所需的时间。
import time
import functools
def timer(func):
"""测量函数执行时间的装饰器"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""这是包装器函数"""
start_time = time.perf_counter() # 使用高精度计时器
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
print(f"'{func.__name__}' 运行时间: {execution_time:.4f} 秒")
return result
return wrapper
@timer
def slow_function(delay):
"""一个故意执行很慢的函数"""
print(f"开始一个 {delay} 秒的延迟操作...")
time.sleep(delay)
return "操作完成!"
# 测试计时装饰器
result = slow_function(1.5)
print(f"函数返回结果: {result}")
输出:
开始一个 1.5 秒的延迟操作...
'slow_function' 运行时间: 1.5001 秒
函数返回结果: 操作完成!
实践练习
练习 1:基础装饰器修改(易)
要求:
修改上面的 log_calls 装饰器,让它在日志中额外打印出函数的参数信息(位置参数和关键字参数)。
预期输出示例:
[时间] Calling function 'add' with args=(3, 5), kwargs={}
Computing 3 + 5
[时间] Function 'add' finished.
Result: 8
练习 2:编写一个"重试"装饰器(中)
要求:
编写一个名为 retry 的装饰器。它可以让被装饰的函数在发生异常时自动重试,最多重试 max_retries 次(默认为 3)。如果在重试次数内成功,则返回结果;如果用尽重试次数仍然失败,则抛出最后一次的异常。
提示:你需要在装饰器中捕获异常 (try...except)。
代码框架:
def retry(max_retries=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 你的代码写在这里
# 提示:可能需要一个 for 循环来重试,并处理异常
pass
return wrapper
return decorator
# 测试你的装饰器
import random
@retry(max_retries=2)
def unstable_operation():
"""这个函数有70%的几率会失败"""
if random.random() < 0.7:
raise ValueError("操作失败!")
return "操作成功!"
# 调用并观察重试行为
print(unstable_operation())
练习 3:组合装饰器思考(难)
要求: 思考以下代码的输出顺序,并写在注释里,然后运行代码验证你的想法。
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def say(text):
return text
# 输出顺序是怎样的?请写出你的答案,然后运行代码验证。
print(say("Hello"))
常见错误
- 忘记返回包装器函数:装饰器的核心是返回
wrapper函数本身,而不是调用它。错误:return wrapper()。正确:return wrapper。 - 丢失原函数信息:不使用
@functools.wraps(func),会导致被装饰函数的__name__、__doc__等属性丢失,全部变成wrapper的信息。这不利于调试和自省。 - 装饰器内部逻辑错误:在
wrapper函数内部,忘记调用原始的func,导致原函数的核心功能丢失。 - 对带参数的函数束手无策:装饰器中的
wrapper函数应使用*args, **kwargs来接收任意参数,以确保能包装任何签名的函数。这是编写通用装饰器的关键。 - 混淆
@decorator和@decorator():当装饰器本身需要参数时,需要使用带括号的语法(如@decorator(arg)),这实际上是调用了一个"装饰器工厂"函数。本课中retry(max_retries=2)就是这种情况。基础的无参装饰器用@decorator。
小结
在本课中,我们学习了 Python 装饰器的基础:
- 核心思想:装饰器是一个接受函数并返回新函数的函数,用于在不修改原函数代码的情况下,为其添加额外功能。
- 实现方式:通过定义一个内部函数
wrapper,在wrapper中调用原函数并加入额外逻辑。 - 语法糖:使用
@decorator_name语法可以简洁地将装饰器应用到函数上。 - 最佳实践:始终使用
@functools.wraps(func)来保留原函数的元信息,并使用*args, **kwargs使装饰器通用。 - 应用场景:日志记录、性能测试(计时)、权限校验、输入验证、缓存等通用功能非常适合用装饰器来实现。
装饰器是Python中强大且优雅的特性,是理解许多高级框架(如 Flask, Django)源码的关键。掌握了基础,下一课我们将学习如何创建带参数的装饰器,让你的装饰器更加灵活强大!