Python 装饰器
Python 装饰器(Decorator)是一种高级语法,用于在不修改原函数代码的前提下,动态扩展函数或类的功能。它本质上是一个接收函数 / 类作为参数,并返回新函数 / 类的可调用对象,是函数式编程的典型应用。
1、核心原理
装饰器的核心依赖于 Python 的两个特性:
- 函数:函数可以作为参数传递、作为返回值返回、赋值给变量。
- 闭包:嵌套函数可以访问外层函数的变量和参数(即使外层函数已执行完毕)。
1.1 装饰器的基本结构
一个最简单的装饰器由外层函数(接收原函数) 和内层函数(包装原函数) 组成,最终返回内层函数:
def decorator(func): # 外层函数:接收原函数作为参数
def wrapper(*args, **kwargs): # 内层函数:包装原函数(*args, **kwargs 兼容任意参数)
print("装饰器逻辑:执行前") # 扩展功能(执行前)
result = func(*args, **kwargs) # 调用原函数
print("装饰器逻辑:执行后") # 扩展功能(执行后)
return result # 返回原函数结果
return wrapper # 返回内层函数
# 使用装饰器(语法糖:@装饰器名)
@decorator
def target_func():
print("原函数逻辑")
# 调用被装饰的函数
target_func()
执行结果:
装饰器逻辑:执行前
原函数逻辑
装饰器逻辑:执行后
说明:@decorator 等价于 target_func = decorator(target_func),即原函数被装饰器返回的 wrapper 函数替换。
1.2 装饰器的执行时机
装饰器在模块加载时(导入时) 就会执行,而非函数调用时。例如:
print("模块加载中...")
def decorator(func):
print(f"装饰器执行:装饰 {func.__name__}")
return func
@decorator
def func1():
pass
@decorator
def func2():
pass
print("模块加载完成")
执行结果(先执行装饰器,再执行后续代码):
模块加载中...
装饰器执行:装饰 func1
装饰器执行:装饰 func2
模块加载完成
2、进阶用法
2.1 带参数的装饰器
若需要给装饰器传递参数(如日志的级别、缓存的过期时间),需在外层再套一层 “参数接收函数”:
def log_decorator(level): # 最外层:接收装饰器参数
def decorator(func): # 中间层:接收原函数
def wrapper(*args, **kwargs):
print(f"[{level}] 函数 {func.__name__} 开始执行") # 使用装饰器参数
result = func(*args, **kwargs)
print(f"[{level}] 函数 {func.__name__} 执行结束")
return result
return wrapper
return decorator
# 使用带参数的装饰器
@log_decorator(level="INFO")
def add(a, b):
return a + b
print(add(1, 2))
执行结果:
[INFO] 函数 add 开始执行
[INFO] 函数 add 执行结束
3
2.2 类装饰器
类也可以作为装饰器,核心是实现 __call__ 方法(使类实例可调用):
class TimerDecorator:
def __init__(self, func): # 初始化时接收原函数
self.func = func
def __call__(self, *args, **kwargs): # 调用时执行装饰逻辑
import time
start = time.time()
result = self.func(*args, **kwargs) # 调用原函数
end = time.time()
print(f"函数 {self.func.__name__} 执行耗时:{end - start:.2f}s")
return result
# 使用类装饰器
@TimerDecorator
def slow_func(seconds):
import time
time.sleep(seconds) # 模拟耗时操作
slow_func(1) # 执行被装饰的函数
执行结果:
函数 slow_func 执行耗时:1.00s
2.3 装饰器的嵌套
多个装饰器可以叠加使用,嵌套装饰器的核心是装饰顺序和调用顺序的区别,理解这两个顺序是掌握嵌套装饰器的关键。
# 定义两个装饰器
def decorator_a(func):
def wrapper(*args, **kwargs):
print("装饰器 A 开始")
result = func(*args, **kwargs)
print("装饰器 A 结束")
return result
return wrapper
def decorator_b(func):
def wrapper(*args, **kwargs):
print("装饰器 B 开始")
result = func(*args, **kwargs)
print("装饰器 B 结束")
return result
return wrapper
# 嵌套应用装饰器
@decorator_a
@decorator_b
def target():
print("执行目标函数")
# 调用目标函数
target()
执行结果:
装饰器 A 开始
装饰器 B 开始
执行目标函数
装饰器 B 结束
装饰器 A 结束
2.3.1 装饰顺序:从下到上(“包裹顺序”)
装饰器的应用过程发生在函数定义时,本质是对函数的 “层层包裹”。对于上面的例子:
@decorator_a
@decorator_b
def target(): ...
等价于手动执行:
# 第一步:用 decorator_b 装饰原始 target,得到新函数 wrapper_b
wrapper_b = decorator_b(target)
# 第二步:用 decorator_a 装饰 wrapper_b,得到最终函数 wrapper_a
target = decorator_a(wrapper_b)
即:先执行下方的装饰器,再执行上方的装饰器,最终得到的 target 是最外层装饰器(decorator_a)返回的 wrapper 函数。
2.3.2 调用顺序:从外到内(“执行顺序”)
当调用被装饰的函数(如 target())时,执行流程是从最外层装饰器的 wrapper 开始,逐层进入内层,最后执行原始函数,再逐层返回。
以上面的 target() 调用为例,执行流程拆解:
- 调用
target()实际是调用decorator_a返回的wrapper_a; wrapper_a中先执行自身逻辑(print("装饰器 A 开始")),然后调用func(此时func是decorator_b返回的wrapper_b);wrapper_b中先执行自身逻辑(print("装饰器 B 开始")),然后调用func(此时func是原始target函数);- 执行原始
target函数(print("执行目标函数")); - 原始函数执行完毕,返回结果给
wrapper_b,wrapper_b执行后续逻辑(print("装饰器 B 结束")); wrapper_b执行完毕,返回结果给wrapper_a,wrapper_a执行后续逻辑(print("装饰器 A 结束"));- 最终结果返回给调用者。
简言之:调用时先执行外层装饰器的 “前置逻辑”,再执行内层装饰器的 “前置逻辑”,然后是原始函数,最后按相反顺序执行各装饰器的 “后置逻辑”。
2.4 保留原函数元信息
装饰器默认会覆盖原函数的元信息(如 __name__、__doc__),需使用 functools.wraps 修复:
import functools
def bad_decorator(func):
def wrapper():
func()
return wrapper # 未保留元信息
def good_decorator(func):
@functools.wraps(func) # 保留原函数元信息
def wrapper():
func()
return wrapper
@bad_decorator
def f1():
"""f1 的文档字符串"""
pass
@good_decorator
def f2():
"""f2 的文档字符串"""
pass
print(f1.__name__) # 输出:wrapper(错误)
print(f1.__doc__) # 输出:None(错误)
print(f2.__name__) # 输出:f2(正确)
print(f2.__doc__) # 输出:f2 的文档字符串(正确)
2.5 装饰类的方法
2.5.1 装饰实例方法
实例方法是类中最常见的方法,第一个参数默认是 self(指向实例本身)。装饰实例方法时,装饰器需要正确传递 self 及其他参数。
用装饰器记录实例方法的调用日志
import functools
# 定义装饰器:记录方法调用的参数、返回值
def log_instance_method(func):
@functools.wraps(func) # 保留原方法的元信息(如 __name__、__doc__)
def wrapper(self, *args, **kwargs):
# 打印实例信息、方法名、参数
print(f"[日志] 实例 {self} 调用方法 {func.__name__},参数:{args},关键字参数:{kwargs}")
# 调用原始方法(注意传递 self)
result = func(self, *args, **kwargs)
# 打印返回值
print(f"[日志] 方法 {func.__name__} 返回:{result}")
return result
return wrapper
# 定义类,用装饰器装饰实例方法
class User:
def __init__(self, name):
self.name = name # 实例属性
# 用装饰器装饰实例方法
@log_instance_method
def greet(self, prefix):
"""向用户打招呼"""
return f"{prefix},我是 {self.name}"
# 测试
user = User("Alice")
user.greet("你好")
执行结果:
[日志] 实例 <__main__.User object at 0x102b5a4d0> 调用方法 greet,参数:('你好',),关键字参数:{}
[日志] 方法 greet 返回:你好,我是 Alice
关键说明:
- 装饰器的
wrapper函数第一个参数必须是self(或用*args兼容),否则无法传递实例引用; func(self, *args, **kwargs)确保原始方法能正确接收self及其他参数;functools.wraps(func)保留原方法的元信息(如user.greet.__name__仍为greet,而非wrapper)。
2.5.2 装饰类方法
类方法用 @classmethod 装饰,第一个参数默认是 cls(指向类本身)。装饰类方法时,需注意装饰器与 @classmethod 的顺序,以及 cls 参数的传递。
用装饰器验证类方法的参数
import functools
# 定义装饰器:检查类方法的参数是否为正数
def validate_positive(func):
@functools.wraps(func)
def wrapper(cls, *args, **kwargs):
# 检查所有位置参数是否为正数
for arg in args:
if not isinstance(arg, (int, float)) or arg <= 0:
raise ValueError(f"类方法 {func.__name__} 接收无效参数:{arg}(必须为正数)")
# 调用原始类方法(传递 cls)
return func(cls, *args, **kwargs)
return wrapper
# 定义类,用装饰器装饰类方法
class MathUtils:
# 注意装饰器顺序:先应用功能装饰器,再用 @classmethod(从下到上装饰)
@classmethod
@validate_positive
def multiply(cls, a, b):
"""计算两个正数的乘积(类方法)"""
return a * b
# 测试
print(MathUtils.multiply(3, 4)) # 正常调用:输出 12
MathUtils.multiply(-2, 5) # 触发验证失败:ValueError
执行结果:
12
ValueError: 类方法 multiply 接收无效参数:-2(必须为正数)
关键说明:
- 装饰器顺序:
@classmethod必须放在功能装饰器的下方(即先被装饰),因为@classmethod会将方法转换为类方法,功能装饰器需要装饰转换后的方法; wrapper函数第一个参数是cls,确保原始类方法能接收类引用;- 类方法操作的是类本身(如
cls.__name__),而非实例,装饰器可基于类属性做校验。
2.5.3 装饰静态方法
静态方法用 @staticmethod 装饰,无默认参数(与普通函数类似)。装饰静态方法时,处理逻辑与装饰普通函数一致。
用装饰器记录静态方法的执行时间
import functools
import time
# 定义装饰器:记录方法执行时间
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # 静态方法无 self/cls,直接传递参数
end = time.time()
print(f"[计时] 静态方法 {func.__name__} 执行耗时:{end - start:.4f} 秒")
return result
return wrapper
# 定义类,用装饰器装饰静态方法
class StringUtils:
# 装饰器顺序:@staticmethod 在下,功能装饰器在上
@staticmethod
@timer
def reverse(s):
"""反转字符串(静态方法)"""
time.sleep(0.1) # 模拟耗时操作
return s[::-1]
# 测试
print(StringUtils.reverse("hello")) # 输出:olleh
执行结果:
[计时] 静态方法 reverse 执行耗时:0.1002 秒
olleh
关键说明:
- 静态方法无
self或cls,装饰器的wrapper函数直接用*args, **kwargs接收参数即可; - 装饰器顺序与类方法类似:
@staticmethod在下,功能装饰器在上,确保装饰的是静态方法。
3、装饰器的应用场景
-
日志记录:自动记录函数的调用参数、返回值、执行时间。
-
权限验证:调用函数前检查用户权限(如登录状态)。
def login_required(func): @functools.wraps(func) def wrapper(user, *args, **kwargs): if not user.is_login: raise PermissionError("请先登录") return func(user, *args, **kwargs) return wrapper @login_required def pay(user, amount): print(f"{user.name} 支付 {amount} 元") -
缓存结果:缓存函数的计算结果,避免重复计算。
from functools import lru_cache @lru_cache(maxsize=128) def expensive_calculation(x, y): # 模拟耗时计算 time.sleep(1) return x * y + x**2 + y**2 # 第一次调用会计算 result1 = expensive_calculation(10, 20) # 第二次相同参数的调用会直接从缓存返回 result2 = expensive_calculation(10, 20) -
异常处理:统一捕获函数执行中的异常,避免程序崩溃。
import functools from typing import Callable, Any, Type, Tuple, Optional def exception_handler( exceptions: Tuple[Type[Exception], ...] = (Exception,), default: Any = None, re_raise: bool = False, error_msg: Optional[str] = None ) -> Callable: """ 异常处理装饰器:捕获指定异常并统一处理 :param exceptions: 需要捕获的异常类型(元组),默认捕获所有Exception :param default: 异常发生时返回的默认值(若re_raise为False) :param re_raise: 捕获异常后是否重新抛出,默认False(不抛出) :param error_msg: 自定义错误提示信息,默认为异常自带消息 :return: 装饰后的函数 """ def decorator(func: Callable) -> Callable: @functools.wraps(func) # 保留原函数元信息 def wrapper(*args: Any, **kwargs: Any) -> Any: try: # 执行原函数 return func(*args, **kwargs) except exceptions as e: # 若需要重新抛出异常,交给上层处理 if re_raise: raise # 保持原异常类型和堆栈 # 否则返回默认值 return default return wrapper return decorator # 示例1:捕获ValueError和TypeError,返回默认值 @exception_handler( exceptions=(ValueError, TypeError), default=-1, error_msg="数值转换失败" ) def parse_number(s: str) -> int: """将字符串转换为整数""" return int(s) # 示例2:捕获IOError,重新抛出异常(让调用者处理) @exception_handler( exceptions=(IOError,), re_raise=True, error_msg="文件读取失败" ) def read_file(path: str) -> str: """读取文件内容""" with open(path, "r") as f: return f.read()
浙公网安备 33010602011771号