装饰器是 Python 最优雅也最"劝退"的特性之一:它能在不改动原函数代码的前提下,给函数附加日志、计时、缓存等能力。一旦看懂它的本质,你会发现它只是一层"函数包函数"的语法糖。
1. 函数是一等公民
Python 里函数可以像变量一样赋值、传参、返回、嵌套定义,这是装饰器能成立的前提:
def greet(name):
return f"你好,{name}"
say = greet # 函数也是对象,可以赋值
print(say("Ada")) # 你好,Ada
2. 最简单的装饰器
装饰器本质是"接收函数、返回新函数"的高阶函数,@ 是它的语法糖:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() + "!"
return wrapper
@shout
def hello(name):
return f"hello {name}"
print(hello("python")) # HELLO PYTHON!
3. 保留函数信息:functools.wraps
不加 wraps,被装饰函数的 __name__ 会变成 wrapper,影响调试与文档生成:
from functools import wraps
def logger(func):
@wraps(func) # 复制原函数的名字和文档
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def add(a, b):
"""两数相加"""
return a + b
print(add.__name__, add.__doc__) # add 两数相加
4. 带参数的装饰器
想给装饰器本身传参,就再包一层"工厂函数":
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def ping():
print("ping")
ping() # 连续打印三次 ping
5. 实战:计时器与缓存
两个最常用的装饰器场景:统计耗时、缓存重复计算。lru_cache 是标准库自带的记忆化工具:
import time
from functools import wraps, lru_cache
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.perf_counter() - t0:.3f}s")
return result
return wrapper
@timer
@lru_cache(maxsize=128) # 缓存相同参数的返回值
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(35))
6. 使用建议
- 别滥用:装饰器会隐藏调用细节,团队项目里保持克制,优先用显式函数。
- 总是加 @wraps:保留函数元信息,否则调试和文档都会受影响。
- 顺序即嵌套:@timer 在上、@lru_cache 在下,执行顺序是"先缓存、后计时"。
- 类装饰器:实现 __call__ 的类也能当装饰器,适合需要维护状态的场景。
💡 学习建议:读一读 Flask、Click 等框架源码里 @app.route 这类装饰器的实现,吃透"工厂函数 + wraps + 闭包"三件套,装饰器就彻底通了。