Python 装饰器(Decorator)完全入门与实践
你有没有遇到过这种场景?
写完了一堆核心业务函数,突然想给每个函数加个执行日志;
又或者发现几个性能敏感的函数,需要批量加上耗时统计;
如果要直接在每个函数开头结尾加重复代码,不仅麻烦,后续维护起来还要改一堆地方——这时候,Python 装饰器就成了你的救星!
1. 装饰器基础概念
装饰器本质上是一种高阶函数,要彻底理解它,得先搞懂两个前置知识点。
1.1 函数也是对象
在 Python 中,函数和整数、字符串、列表一样,都是“一等公民”:
- 可以赋值给变量
- 可以作为另一个函数的参数
- 可以作为另一个函数的返回值
- 可以存在列表/字典里
先看赋值的小例子:
def get_current_date():
print('2024-06-01')
f = get_current_date # 不带括号!是把函数对象本身赋值给变量
f() # 带括号才是调用函数
# 输出: 2024-06-01
# 每个函数对象都有专属元属性,比如 __name__ 显示函数名
print(get_current_date.__name__) # 输出: get_current_date
print(f.__name__) # 输出: get_current_date(变量f只是函数的“别名”)
1.2 高阶函数
简单来说,接受函数为参数,或返回函数的函数,就是高阶函数。
刚才的赋值例子已经满足了“函数作为对象”的条件,我们再写一个简单的高阶前置函数:
def prepare_exec(func):
print("=== 准备执行函数 ===")
func() # 直接调用传入的函数
print("=== 函数执行完毕 ===")
def say_hello():
print("Hello Python Deco!")
prepare_exec(say_hello)
# 输出:
# === 准备执行函数 ===
# Hello Python Deco!
# === 函数执行完毕 ===
这个例子可以工作,但每次调用 say_hello 都要手动包一层 prepare_exec,太繁琐了。能不能让 prepare_exec 直接“替换” say_hello,以后直接用 say_hello() 就行?
这就引出了装饰器的核心雏形:返回新函数的高阶函数。
2. 入门:实现第一个装饰器
2.1 基础装饰器 + 语法糖
我们修改刚才的 prepare_exec,让它返回一个“包装函数” wrapper,这个包装函数会先执行额外功能,再调用原函数:
def log(func):
def wrapper(*args, **kwargs): # *args/kwargs 接受任意参数,原函数怎么调,wrapper就能怎么调
print(f'调用 {func.__name__}() 函数')
return func(*args, **kwargs) # 必须返回原函数的返回值,否则原功能就丢了!
return wrapper
然后用 Python 提供的装饰器语法糖 @,把它贴在需要增强的函数头上:
@log
def get_current_date():
print('2024-06-01')
@log
def add(a, b):
return a + b
# 调用方式完全不变!
get_current_date()
# 输出:
# 调用 get_current_date() 函数
# 2024-06-01
result = add(1, 2)
print(result)
# 输出:
# 调用 add() 函数
# 3
注意! @log 放在函数上面,等价于 Python 解释器自动帮你执行了:
get_current_date = log(get_current_date)
2.2 保留原函数的元信息
刚才的装饰器有个小bug:包装后函数的元属性变了!
print(get_current_date.__name__) # 输出: wrapper
print(help(get_current_date)) # 输出会指向 wrapper 的 docstring(如果有的话)
这会给后续的代码调试、文档生成、依赖元属性的工具带来麻烦。修复方式很简单,用 Python 标准库的 functools.wraps:
import functools # 引入标准库
def log(func):
# 自动把原函数 func 的 __name__、__doc__、__module__ 等元属性复制给 wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'调用 {func.__name__}() 函数')
return func(*args, **kwargs)
return wrapper
现在再试,元属性就正常了:
print(get_current_date.__name__) # 输出: get_current_date
3. 进阶:带自定义参数的装饰器
如果想给装饰器加个前缀,比如有时用 [INFO] 有时用 [DEBUG],怎么办?
需要再嵌套一层函数!最外层接收自定义参数,中间层是原来的装饰器,最内层是包装函数:
import functools
def log(prefix="[INFO]"): # 最外层:接受自定义参数,给个默认值让它更通用
def decorator(func): # 中间层:就是原来的“返回wrapper的高阶函数”
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'{prefix} 调用 {func.__name__}()')
return func(*args, **kwargs)
return wrapper
return decorator
使用方式分两种:
# 带自定义参数的用法
@log(prefix="[DEBUG]")
def add(a, b):
return a + b
# 用默认参数的用法(必须带括号!如果不带,请看4.2的通用装饰器)
@log()
def get_current_date():
print('2024-06-01')
三层嵌套的原理可以分两步看:
- 先执行
log(prefix="[DEBUG]"),拿到返回值 decorator
- 再执行
@decorator,也就是 add = decorator(add)
4. 实践:两个常用装饰器
4.1 计算函数执行时间
性能优化时经常用到,注意用 time.perf_counter() 代替 time.time(),因为前者是高精度短时间计时器,不受系统时间调整的影响:
import time
import functools
def metric(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f'{func.__name__} 执行耗时: {(end - start)*1000:.2f}ms')
return result
return wrapper
@metric
def fast_add(x, y):
time.sleep(0.0012)
return x + y
@metric
def slow_multiply(x, y, z):
time.sleep(0.1234)
return x * y * z
4.2 通用日志装饰器(两种用法都支持)
如果觉得带默认参数的装饰器必须写 @log() 太麻烦,可以优化成“带不带括号都能用”的通用版本:
import functools
def log(text_or_func=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if callable(text_or_func):
# 不带括号的情况:text_or_func 其实是原函数
print(f'调用 {func.__name__}()')
else:
# 带括号的情况:text_or_func 是自定义前缀
print(f'{text_or_func} 调用 {func.__name__}()')
return func(*args, **kwargs)
return wrapper
# 判断第一个参数是函数还是文本
if callable(text_or_func):
func = text_or_func
text_or_func = None
return decorator(func)
else:
return decorator
两种用法都完美兼容:
@log
def func1():
pass
@log("[DEBUG]")
def func2():
pass
5. 高级:用类实现装饰器
装饰器也可以用类来实现,主要优势有两个:
- 可以方便地保存状态(比如统计函数被调用多少次)
- 可以通过继承扩展装饰器功能
5.1 简单的类装饰器
类装饰器需要实现 __init__ 和 __call__ 两个方法:
__init__:接收原函数作为参数,并保留元信息
__call__:相当于之前的 wrapper,执行额外功能并调用原函数
import functools
class Logger:
def __init__(self, func):
self.func = func
# 类装饰器没有 @functools.wraps 方便,用 update_wrapper 代替
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f'调用 {self.func.__name__}()')
return self.func(*args, **kwargs)
@Logger
def get_current_date():
print('2024-06-01')
5.2 带状态的类装饰器(统计调用次数)
这是类装饰器最常用的场景之一:
import functools
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0 # 用实例属性保存调用次数
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 已被调用 {self.count} 次")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
# 输出:
# say_hello 已被调用 1 次
# Hello!
# say_hello 已被调用 2 次
# Hello!
6. 装饰器最佳实践
- 始终保留原函数元信息:用
functools.wraps(函数装饰器)或 functools.update_wrapper(类装饰器)。
- 单一职责原则:一个装饰器只做一件事,比如计时装饰器别顺便记日志,日志装饰器别顺便加权限。
- 保持可配置性:尽量让装饰器接受可选参数,比如之前的通用日志装饰器。
- 避免过度使用:装饰器会增加一层抽象,套三四层以上会让代码执行流程变得不直观,调试困难。
7. 总结
现在再回头看开头的批量加日志、批量加计时的问题,是不是觉得很简单了?只要写好一个装饰器,往所有需要的函数头上加个 @ 就行,完全不用修改核心业务代码。
装饰器的核心价值在于:
- 非侵入式扩展:不修改原函数的一行代码
- 处理横切关注点:日志、计时、权限检查、Python自带的
@functools.lru_cache缓存等,和核心业务无关但又很多地方用到的功能
- 代码更简洁、更Pythonic
掌握装饰器,绝对能让你的Python代码提升一个档次!现在就去给你的现有函数试试吧!