python装饰器简单案例实践
一、装饰器是什么?先搞懂核心原理
在讲装饰器之前,我们需要先明确 Python 的一个核心特性:函数是 “一等公民”。这意味着函数可以像变量一样被赋值、作为参数传递,甚至作为其他函数的返回值。而装饰器的本质,就是基于这个特性实现的 “函数包装器”。
1.1 装饰器的定义
装饰器(Decorator)是一个接收函数作为参数,并返回一个新函数的特殊函数。它的核心作用是:在不修改原函数代码的前提下,为原函数添加额外功能(比如日志、计时、权限判断等)。
举个最直观的例子:假设我们有一个计算两数之和的函数 add()
,现在需要给它增加 “打印计算日志” 的功能,但不能修改 add()
本身的代码 —— 这时候装饰器就能派上用场。
二、从 0 开始写第一个装饰器
我们先通过 “手动包装” 的方式理解装饰器的逻辑,再过渡到标准的 @
语法。
2.1 步骤 1:定义原函数
首先,我们有一个简单的原函数 add
,用于计算两个数的和:
def add(a, b):"""计算两数之和"""return a + b# 正常调用
print(add(2, 3)) # 输出:5
2.2 步骤 2:定义装饰器函数
我们定义一个 log_decorator
函数,它接收一个函数 func
作为参数,并返回一个新的函数 wrapper
(包装函数)。在 wrapper
中,我们会先执行 “打印日志” 的额外逻辑,再调用原函数 func
:
def log_decorator(func):"""用于添加日志功能的装饰器"""# 定义包装函数,*args 和 **kwargs 用于接收原函数的任意参数def wrapper(*args, **kwargs):# 额外功能:打印日志(记录函数名和参数)print(f"调用函数:{func.__name__},参数:{args}, {kwargs}")# 调用原函数,并获取返回值result = func(*args, **kwargs)# 额外功能:打印返回值print(f"函数 {func.__name__} 的返回值:{result}")# 返回原函数的返回值(保证原函数调用逻辑不变)return result# 返回包装函数return wrapper
2.3 步骤 3:使用装饰器包装原函数
有两种方式可以使用装饰器:手动赋值和 @
语法糖(推荐)。
方式 1:手动赋值(理解原理)
将原函数 add
作为参数传给 log_decorator
,得到包装后的新函数,再赋值给 add
(相当于 “替换” 了原函数):
# 用装饰器包装 add 函数
add = log_decorator(add)# 现在调用 add,实际执行的是 wrapper 函数
print(add(2, 3))
执行结果如下,可见额外的日志功能已经生效:
调用函数:add,参数:(2, 3), {}
函数 add 的返回值:5
5
方式 2:@
语法糖(实战常用)
Python 提供了 @
符号作为装饰器的语法糖,直接放在原函数定义上方即可,效果和手动赋值完全一致,但代码更简洁:
# 用 @ 语法糖应用装饰器
@log_decorator
def add(a, b):return a + b# 直接调用,自动触发装饰器逻辑
print(add(2, 3)) # 输出结果和上面一致
三、进阶:带参数的装饰器
上面的装饰器是 “固定功能” 的(只能打印固定格式的日志),但实际场景中,我们可能需要让装饰器支持自定义参数(比如让日志输出不同的前缀)。这时候就需要定义带参数的装饰器—— 本质是 “装饰器的工厂函数”。
3.1 带参数装饰器的实现逻辑
带参数的装饰器需要多一层嵌套:
-
最外层函数:接收装饰器的自定义参数,返回一个 “真正的装饰器”(即之前的
log_decorator
); -
中间层函数:接收原函数
func
,返回包装函数wrapper
; -
最内层函数
wrapper
:执行额外逻辑和原函数。
3.2 示例:支持自定义日志前缀的装饰器
比如我们需要让日志开头可以自定义前缀(如 [INFO]
、[DEBUG]
),实现代码如下:
def log_decorator_with_prefix(prefix):"""带参数的装饰器:支持自定义日志前缀"""# 中间层:真正的装饰器(接收原函数)def actual_decorator(func):def wrapper(*args, **kwargs):# 使用装饰器的自定义参数 prefixprint(f"{prefix} 调用函数:{func.__name__},参数:{args}, {kwargs}")result = func(*args, **kwargs)print(f"{prefix} 函数 {func.__name__} 的返回值:{result}")return resultreturn wrapper# 返回真正的装饰器return actual_decorator
3.3 使用带参数的装饰器
使用时需要在 @
后传入装饰器的参数(注意括号不能少):
# 应用带参数的装饰器,指定日志前缀为 [INFO]
@log_decorator_with_prefix("[INFO]")
def add(a, b):return a + b# 调用函数
print(add(2, 3))
执行结果如下,日志前缀已按自定义参数显示:
[INFO] 调用函数:add,参数:(2, 3), {}
[INFO] 函数 add 的返回值:5
5
四、实战:装饰器的 3 个常用场景
装饰器的应用非常广泛,以下是 3 个企业开发中高频使用的场景,直接复制代码即可复用。
4.1 场景 1:函数执行时间统计
用于定位耗时函数,优化性能:
import timedef time_decorator(func):"""统计函数执行时间的装饰器"""def wrapper(*args, **kwargs):start_time = time.time() # 记录开始时间result = func(*args, **kwargs) # 执行原函数end_time = time.time() # 记录结束时间# 打印执行时间(保留4位小数)print(f"函数 {func.__name__} 执行时间:{end_time - start_time:.4f} 秒")return resultreturn wrapper# 测试:统计一个耗时操作(比如循环100万次)
@time_decorator
def slow_operation():for i in range(10**6):passslow_operation() # 输出:函数 slow_operation 执行时间:0.0456 秒(值会因机器而异)
4.2 场景 2:接口权限校验
在 Web 开发中(如 Flask/Django),用于校验用户是否登录,未登录则拒绝访问:
# 模拟用户登录状态(实际项目中会从Session/Token中获取)
current_user = {"is_login": False, "username": "guest"}def login_required(func):"""校验用户是否登录的装饰器"""def wrapper(*args, **kwargs):if not current_user["is_login"]:# 未登录,返回错误信息return "Error:请先登录!"# 已登录,执行原函数(比如返回用户中心页面)return func(*args, **kwargs)return wrapper# 测试:需要登录才能访问的接口
@login_required
def user_center():return f"欢迎 {current_user['username']} 进入用户中心!"# 未登录时调用
print(user_center()) # 输出:Error:请先登录!# 模拟登录后再调用
current_user["is_login"] = True
current_user["username"] = "pythoner"
print(user_center()) # 输出:欢迎 pythoner 进入用户中心!
4.3 场景 3:函数调用缓存
对于计算密集型函数(如斐波那契数列、大数字运算),用装饰器缓存已计算的结果,避免重复计算,提升性能:
def cache_decorator(func):"""缓存函数调用结果的装饰器(基于字典)"""cache = {} # 用于存储缓存结果,key是函数参数,value是返回值def wrapper(*args):# 如果参数在缓存中,直接返回缓存结果if args in cache:print(f"命中缓存:{func.__name__}{args}")return cache[args]# 未命中缓存,执行原函数并缓存结果result = func(*args)cache[args] = resultprint(f"缓存新增:{func.__name__}{args} = {result}")return resultreturn wrapper# 测试:计算斐波那契数列(递归实现,未缓存时性能极差)
@cache_decorator
def fibonacci(n):if n <= 1:return nreturn fibonacci(n-1) + fibonacci(n-2)# 第一次调用:计算并缓存
print(fibonacci(5))
# 第二次调用:直接命中缓存
print(fibonacci(5))
执行结果如下,可见第二次调用直接复用了缓存:
缓存新增:fibonacci(0) = 0
缓存新增:fibonacci(1) = 1
缓存新增:fibonacci(2) = 1
缓存新增:fibonacci(3) = 2
缓存新增:fibonacci(4) = 3
缓存新增:fibonacci(5) = 5
5
命中缓存:fibonacci(5)
5
注意:Python 标准库
functools
中已经内置了更强大的缓存装饰器lru_cache
,实际项目中推荐直接使用:
from functools import lru_cache@lru_cache(maxsize=None) # maxsize=None 表示不限制缓存大小
def fibonacci(n):if n <= 1:return nreturn fibonacci(n-1) + fibonacci(n-2)
五、注意事项:避免装饰器的 “坑”
5.1 问题 1:原函数的元信息丢失
装饰器会用 wrapper
函数替换原函数,导致原函数的元信息(如 __name__
、__doc__
)丢失。比如:
@log_decorator
def add(a, b):"""计算两数之和"""return a + bprint(add.__name__) # 输出:wrapper(而非期望的 add)
print(add.__doc__) # 输出:None(而非期望的“计算两数之和”)
5.2 解决:使用 functools.wraps
functools.wraps
是 Python 提供的工具,用于将原函数的元信息复制到 wrapper
函数中,解决元信息丢失问题。修改装饰器如下:
from functools import wrapsdef log_decorator(func):# 关键:用 wraps 装饰 wrapper,传入原函数 func@wraps(func)def wrapper(*args, **kwargs):print(f"调用函数:{func.__name__}")return func(*args, **kwargs)return wrapper# 再次测试
@log_decorator
def add(a, b):"""计算两数之和"""return a + bprint(add.__name__) # 输出:add(正确)
print(add.__doc__) # 输出:计算两数之和(正确)
5.3 问题 2:装饰器的执行顺序
当一个函数被多个装饰器修饰时,装饰器的执行顺序是 “从下到上包装,从上到下执行”。我们通过“计时装饰器和日志装饰器”的实战案例,直观感受这一规律。
实战案例:双装饰器(计时+日志)
首先,确保我们有两个功能明确的装饰器(已补充@wraps
保留元信息):
import time
from functools import wraps# 1. 日志装饰器:打印函数调用和返回日志
def log_decorator(func):@wraps(func)def wrapper(*args, **kwargs):print(f"【日志装饰器】开始调用函数:{func.__name__}")result = func(*args, **kwargs)print(f"【日志装饰器】函数 {func.__name__} 调用结束,返回值:{result}")return resultreturn wrapper# 2. 计时装饰器:统计函数执行时间
def time_decorator(func):@wraps(func)def wrapper(*args, **kwargs):print(f"【计时装饰器】开始计时,函数:{func.__name__}")start_time = time.time()result = func(*args, **kwargs)end_time = time.time()print(f"【计时装饰器】计时结束,函数 {func.__name__} 执行时间:{end_time - start_time:.4f} 秒")return resultreturn wrapper
接着,用两个装饰器修饰同一个原函数(注意装饰器的书写顺序):
# 原函数:模拟一个简单的业务逻辑(休眠0.5秒,返回两数之和)
@time_decorator # 装饰器1:写在上方
@log_decorator # 装饰器2:写在下方(靠近原函数)
def calculate_sum(a, b):time.sleep(0.5) # 模拟业务耗时return a + b# 调用函数,观察执行顺序
print("最终结果:", calculate_sum(3, 5))
执行结果与分析
运行上述代码,输出如下:
【计时装饰器】开始计时,函数:calculate_sum
【日志装饰器】开始调用函数:calculate_sum
【日志装饰器】函数 calculate_sum 调用结束,返回值:8
【计时装饰器】计时结束,函数 calculate_sum 执行时间:0.5012 秒
最终结果: 8
结合结果,我们拆解 “包装顺序” 和 “执行顺序”:
-
包装顺序(从下到上):
原函数calculate_sum
先被下方的log_decorator
包装(形成log_wrapper
),再被上方的time_decorator
包装(形成time_wrapper
)。最终调用的calculate_sum
,实际是time_wrapper(log_wrapper(calculate_sum))
。 -
执行顺序(从上到下):
调用函数时,先进入上方装饰器time_decorator
的逻辑(打印 “开始计时”),再进入下方装饰器log_decorator
的逻辑(打印 “开始调用”),接着执行原函数,最后按 “日志装饰器→计时装饰器” 的顺序收尾。
通过这个案例可以明确:装饰器写在上方的,先执行逻辑;写在下方的,后执行逻辑。
六、总结
装饰器是 Python 中极具优雅性和实用性的特性,核心是 “不修改原函数代码,为函数添加额外功能”。掌握它需要记住以下 3 点:
-
本质:装饰器是 “接收函数、返回函数” 的函数,基于 “函数是一等公民” 实现;
-
用法:用
@装饰器名
语法糖应用装饰器,带参数的装饰器需多一层嵌套; -
实战:常用场景包括日志、计时、权限校验、缓存,记得用
functools.wraps
保留原函数元信息。