Python装饰器完全指南:从基础语法到高级实战用法
点击下载TXTPython装饰器完全指南:从基础语法到高级实战用法
装饰器(Decorator)是Python中最优雅、最强大的特性之一。它允许我们在不修改原函数代码的前提下,为函数添加额外的功能。从Web框架的路由注册,到权限校验、日志记录、性能监控,装饰器无处不在。本文将从零开始,带你系统掌握装饰器的每一个细节,并配有大量可运行的代码示例。
一、理解装饰器的本质
在Python中,函数是一等对象(First-class Object),这意味着函数可以赋值给变量、作为参数传递、作为返回值返回。装饰器正是利用了这一特性:它接收一个函数,返回一个增强了的新函数。
理解装饰器的关键在于,@decorator语法糖本质上只是一个函数调用的简写形式。下面两种写法完全等价:
# 写法一:使用 @ 语法糖
@my_decorator
def my_function():
pass
# 写法二:手动调用装饰器
def my_function():
pass
my_function = my_decorator(my_function)
第二种写法清楚地展示了装饰器的工作原理:它把原始函数传给装饰器,然后用返回的新函数替换掉原来的函数名绑定。理解了这一点,后面所有复杂的装饰器变体都不会让你困惑。
二、最简单的装饰器
让我们从最基础的装饰器开始,逐步深入。一个简单的装饰器通常包含两层:外层函数接收被装饰的函数,内层wrapper函数执行增强逻辑。
def log_call(func):
"""记录函数调用的简单装饰器"""
def wrapper(*args, **kwargs):
print(f"[LOG] 正在调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"[LOG] 函数 {func.__name__} 执行完毕")
return result
return wrapper
@log_call
def calculate_sum(a, b):
"""计算两个数的和"""
return a + b
result = calculate_sum(10, 20)
print(f"结果: {result}")
# 输出:
# [LOG] 正在调用函数: calculate_sum
# [LOG] 函数 calculate_sum 执行完毕
# 结果: 30
这里的*args, **kwargs确保装饰器能接受任意参数,这是一个好习惯,让装饰器可以通用化。
三、带参数的装饰器
当你需要让装饰器本身也接受参数时(比如控制日志级别、重试次数等),就需要再增加一层嵌套,形成三层函数结构。这是装饰器最让人困惑的地方,但实际上逻辑非常清晰:最外层接收装饰器的参数,中间层接收被装饰的函数,最内层是实际的wrapper。
import time
def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
"""
重试装饰器 - 函数执行失败时自动重试
参数:
max_attempts: 最大重试次数(默认3次)
delay: 每次重试之间的等待时间(秒)
exceptions: 需要捕获的异常类型元组
"""
def decorator(func):
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
print(f"[重试] {func.__name__} 第{attempt}次失败")
time.sleep(delay)
else:
print(f"[失败] {func.__name__} 已重试{max_attempts}次")
raise last_exception
return wrapper
return decorator
# 使用示例:网络请求自动重试
@retry(max_attempts=5, delay=2.0, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
"""模拟网络请求"""
import random
if random.random() < 0.7:
raise ConnectionError(f"无法连接到 {url}")
return f"来自 {url} 的数据"
data = fetch_data("https://api.example.com/data")
带参数的装饰器在Flask中广泛使用,比如@app.route('/path', methods=['GET']),其中的path和methods就是装饰器的参数。
四、functools.wraps:保留函数元信息
使用装饰器后,被装饰函数的__name__、__doc__、__module__等元信息都会变成wrapper函数的信息。这在调试、生成文档时会造成困扰。functools.wraps装饰器可以解决这个问题,它会将被装饰函数的元信息复制到wrapper上。
from functools import wraps
def log_call(func):
@wraps(func) # 关键:保留原函数的元信息
def wrapper(*args, **kwargs):
"""这是wrapper的文档字符串"""
print(f"[LOG] 调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def process_data(data):
"""处理数据的函数,接收一个列表参数"""
return [x * 2 for x in data]
# 没有 @wraps 时,以下值都是 wrapper 的
print(process_data.__name__) # process_data(而非 wrapper)
print(process_data.__doc__) # 处理数据的函数,接收一个列表参数
print(process_data.__module__) # __main__
在实际项目中,永远使用@wraps(func)装饰wrapper函数。这不仅是一个好习惯,更是团队协作中的基本要求——否则你的同事在调试时会非常痛苦。
五、类装饰器
除了函数形式的装饰器,Python还支持使用类来实现装饰器。类装饰器通过实现__call__方法让实例变为可调用对象。类装饰器的优势在于可以通过属性保存状态,适合需要维护内部状态的场景。
from functools import wraps
class CountCalls:
"""统计函数被调用次数的类装饰器"""
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)
def reset(self):
"""重置计数器"""
self.count = 0
@CountCalls
def say_hello(name):
"""打招呼"""
print(f"Hello, {name}!")
say_hello("Alice") # [统计] say_hello 已被调用 1 次
say_hello("Bob") # [统计] say_hello 已被调用 2 次
say_hello("Charlie") # [统计] say_hello 已被调用 3 次
# 可以访问装饰器实例的属性
print(f"总调用次数: {say_hello.count}") # 3
say_hello.reset() # 重置计数
六、多个装饰器的叠加顺序
在实际开发中,经常需要同时使用多个装饰器,比如同时需要日志记录和权限校验。理解装饰器的叠加顺序非常重要——装饰器的执行顺序是从下到上的(最靠近函数的先执行),但调用顺序是从上到下的。
from functools import wraps
def decorator_a(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("A: 函数执行前")
result = func(*args, **kwargs)
print("A: 函数执行后")
return result
return wrapper
def decorator_b(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("B: 函数执行前")
result = func(*args, **kwargs)
print("B: 函数执行后")
return result
return wrapper
# 等价于: my_func = decorator_a(decorator_b(my_func))
@decorator_a
@decorator_b
def my_func():
print("执行 my_func")
my_func()
# 输出:
# A: 函数执行前
# B: 函数执行前
# 执行 my_func
# B: 函数执行后
# A: 函数执行后
可以把叠加的装饰器想象成洋葱:最外层的装饰器先包上去,调用时从外向内一层层剥开,返回时再从内向外一层层合上。记住一个原则:最靠近函数定义的装饰器最先被应用到函数上。
七、装饰器实战:五大常用模式
7.1 性能计时器
测量函数执行时间是性能优化的第一步。下面这个装饰器支持输出到日志或返回耗时数据。
import time
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def timer(log_result=False):
"""计时装饰器
参数:
log_result: 如果为True,将耗时作为第二个返回值
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
logger.info(f"{func.__name__} 耗时: {elapsed:.6f}秒")
if log_result:
return result, elapsed
return result
return wrapper
return decorator
@timer(log_result=True)
def heavy_computation(n):
"""计算1到n的平方和"""
return sum(i * i for i in range(n))
result, elapsed = heavy_computation(1_000_000)
print(f"平方和: {result}, 耗时: {elapsed:.4f}秒")
7.2 缓存装饰器(Memoization)
对于纯函数(相同输入始终产生相同输出),缓存可以大幅提升性能。Python标准库提供了@functools.lru_cache,但了解其原理也很重要。
from functools import wraps
def memoize(func):
"""手动实现的缓存装饰器"""
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
print(f"[缓存未命中] {func.__name__}{args},计算并缓存")
else:
print(f"[缓存命中] {func.__name__}{args},直接返回")
return cache[args]
wrapper.cache = cache # 暴露缓存,方便调试
wrapper.cache_clear = lambda: cache.clear()
return wrapper
@memoize
def fibonacci(n):
"""计算第n个斐波那契数"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 大部分调用都会命中缓存
print(f"缓存大小: {len(fibonacci.cache)}")
7.3 权限校验装饰器
在Web开发中,权限校验是最常见的装饰器应用场景之一。下面模拟一个简单的用户权限系统。
from functools import wraps
class User:
def __init__(self, name, role):
self.name = name
self.role = role
current_user = User("admin", "admin")
def require_role(*roles):
"""权限校验装饰器:只允许指定角色的用户访问"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if current_user.role not in roles:
raise PermissionError(
f"用户 '{current_user.name}' 角色为 '{current_user.role}',"
f"需要以下角色之一: {roles}"
)
print(f"[权限通过] {current_user.name} ({current_user.role})")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin", "superuser")
def delete_database():
"""危险操作:删除数据库"""
print("数据库已删除!")
@require_role("admin", "editor", "viewer")
def view_report():
"""查看报告"""
print("报告内容:一切正常")
delete_database() # admin角色,通过
view_report() # admin角色,通过
current_user = User("guest", "viewer")
view_report() # viewer角色,通过
7.4 单例模式装饰器
单例模式确保一个类只有一个实例。使用装饰器实现单例比修改类的__new__方法更加优雅和通用。
from functools import wraps
def singleton(cls):
"""单例装饰器:确保一个类只有一个实例"""
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
print(f"[单例] 创建 {cls.__name__} 的新实例")
else:
print(f"[单例] 返回 {cls.__name__} 的已有实例")
return instances[cls]
get_instance._instances = instances
return get_instance
@singleton
class DatabaseConnection:
"""数据库连接类"""
def __init__(self, host="localhost", port=3306):
self.host = host
self.port = port
print(f"连接到数据库: {host}:{port}")
def query(self, sql):
return f"执行查询: {sql}"
db1 = DatabaseConnection("192.168.1.100", 3306)
db2 = DatabaseConnection("10.0.0.1", 5432) # 不会创建新实例
print(db1 is db2) # True
print(db1.host) # 192.168.1.100(第一次的参数)
7.5 类型检查装饰器
Python是动态类型语言,但在关键接口处进行类型检查可以提高代码的健壮性。下面的装饰器在运行时验证函数参数和返回值的类型。
from functools import wraps
def typecheck(**expected_types):
"""运行时类型检查装饰器
用法: @typecheck(name=str, age=int)
"""
def decorator(func):
@wraps(func)
def wrapper(**kwargs):
for param_name, expected_type in expected_types.items():
if param_name in kwargs:
actual_value = kwargs[param_name]
if not isinstance(actual_value, expected_type):
raise TypeError(
f"参数 '{param_name}' 期望类型 {expected_type.__name__},"
f"实际类型 {type(actual_value).__name__}"
)
result = func(**kwargs)
return result
return wrapper
return decorator
@typecheck(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
user = create_user(name="Alice", age=30) # 正常
print(user)
八、装饰器的常见陷阱与避坑指南
陷阱1:忘记使用functools.wraps
前面已经讲过,这里再强调一次。不使用@wraps(func)会导致函数的元信息丢失,影响调试、文档生成和序列化。
陷阱2:装饰器中的可变默认参数
# 错误写法:所有调用共享同一个列表
def append_result(func, results=[]):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
results.append(result) # 危险!所有调用共享同一个列表
return result
return wrapper
# 正确写法:使用闭包内的局部变量
def append_result_correct(func):
results = [] # 每个被装饰的函数有独立的results列表
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
results.append(result)
wrapper.all_results = results
return result
return wrapper
陷阱3:装饰器改变函数签名
即使使用了@wraps,装饰器的wrapper函数的签名仍然是(*args, **kwargs),这会影响IDE的自动补全和类型检查工具。Python 3.3+提供了functools.signature来手动修复,或者使用第三方库wrapt来彻底解决。
from functools import wraps
import inspect
def preserve_signature(func):
"""保留原始函数签名的装饰器辅助"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
九、标准库中的装饰器
Python标准库中内置了多个实用的装饰器,了解它们可以避免重复造轮子:
- @functools.lru_cache(maxsize=128):LRU缓存,线程安全,支持缓存统计
- @functools.total_ordering:只需定义__eq__和一个比较方法,自动补全其他比较方法
- @property:将方法变为属性访问,支持getter/setter/deleter
- @classmethod / @staticmethod:类方法和静态方法
- @dataclasses.dataclass:自动生成__init__、__repr__等方法
- @contextlib.contextmanager:用生成器简化上下文管理器的编写
from functools import lru_cache
@lru_cache(maxsize=256)
def expensive_compute(n):
"""带LRU缓存的计算函数"""
print(f"正在计算 {n}...")
return n ** 3
print(expensive_compute(5)) # 计算并缓存
print(expensive_compute(5)) # 直接从缓存返回
print(expensive_compute.cache_info()) # 查看缓存统计
总结
装饰器是Python中强大而优雅的工具,掌握它需要理解三个核心概念:函数是一等对象、闭包的工作原理、以及@语法糖的本质。从最简单的日志记录到复杂的权限控制、缓存和重试机制,装饰器的应用场景无处不在。
在实际开发中,请始终遵循以下最佳实践:
1. 永远使用@wraps(func)保留函数元信息
2. 装饰器应保持单一职责,不要在一个装饰器中做太多事情
3. 优先使用标准库提供的装饰器(lru_cache、property等),避免重复造轮子
4. 为装饰器编写清晰的文档字符串,说明参数和用法
5. 注意装饰器的叠加顺序,最靠近函数定义的最先被应用
建议从最基础的装饰器开始练习,逐步过渡到带参数的装饰器和类装饰器,最终将这些技巧应用到实际项目中。当你能自如地编写装饰器时,你对Python函数式编程的理解也会上一个新的台阶。