系列:Python 底层设计逻辑|类型:原理拆解型|分发:CSDN / 掘金 / 51CTO / 阿里云 / 知乎 字数:约 1600 字
结论先行:Python 里的装饰器(decorator)本质就是一个"接收函数、返回函数"的高阶函数。@log 写在 def foo() 上面,等价于 foo = log(foo)。它不创造新语法,只是把"函数也能当参数和返回值"这件事用语法糖包了一下。搞懂这一点,你就不会再被嵌套 @ 吓到。
1. 最朴素的理解:装饰器就是一个包装工厂
python
def log(func):
def wrapper(*args, **kwargs):
print(f"调用了 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
def add(a, b):
return a + b
add(1, 2) # 打印:调用了 add,然后返回 3
关键 :@log 这一行,解释器执行时就是 add = log(add)。你写的 add 还是原来的函数,log 把它接过去,套一层 wrapper 再还给你。所以之后调 add,实际跑的是 wrapper。
2. 为什么要写成 @?不用行不行?
行。@ 只是语法糖,完全可以写成:
python
def add(a, b):
return a + b
add = log(add) # 和 @log 等价
为什么 还要 @:因为它把"增强逻辑"贴在函数定义处,一眼就能看出这个函数被加工过。代码读起来是"声明式"的,比在底部再赋值清爽太多。
3. 带参数的装饰器:其实是三层嵌套
python
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def hi():
print("hi")
hi() # 打印三次 hi
拆解 :@repeat(3) 先执行 repeat(3),返回 decorator;再拿 decorator 去包 hi。所以是"函数→返回装饰器→返回 wrapper"三层。看到括号就多一层,这是铁律。
4. 一个真实的坑:__name__ 被改写了
python
print(add.__name__) # 输出 wrapper,不是 add!
问题 :装饰后函数名、文档字符串全变成 wrapper 的,调试和日志会乱。解法是用 functools.wraps:
python
from functools import wraps
def log(func):
@wraps(func) # 把原函数元信息复制过来
def wrapper(*args, **kwargs):
print(f"调用了 {func.__name__}")
return func(*args, **kwargs)
return wrapper
记住 :只要你手写装饰器,第一件事就是加 @wraps(func),别省。
5. 类也能当装饰器
python
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用")
return self.func(*args, **kwargs)
@CountCalls
def hello():
print("hello")
hello(); hello() # 第1次 / 第2次
要点 :类装饰器靠 __call__ 让实例可像函数一样被调用。想给函数挂"状态"(比如调用次数),类比闭包更直观。
对照表:一眼分清几种写法
| 写法 | 等价展开 | 适用场景 |
|---|---|---|
@log |
f = log(f) |
无参数增强(日志/计时/鉴权) |
@repeat(3) |
f = repeat(3)(f) |
需要配置参数的增强 |
类 @CountCalls |
f = CountCalls(f) |
增强过程要保存状态 |
不加 @wraps |
--- | 别这么干,元信息会丢 |
排错清单(收藏级)
@xxx报错"xxx is not callable":八成是把装饰器当函数用了,少写一层返回。- 带参装饰器套错层数 :记住"有括号就多一层",
@a(b)是a(b)(f)。 - 函数名变成 wrapper :忘了
@wraps,加上即可。 - 装饰器顺序影响结果 :多个
@从下往上包,越靠上越外层,顺序不同行为不同。 - 想装饰类里的方法 :
@staticmethod/@property本身也是装饰器,原理一样。
一句话记住 :装饰器就是"函数进、函数出"的包装机,@ 只是把这件事写得更好看。先把 foo = deco(foo) 刻进脑子,所有嵌套都是它的变种。