LOADING
3388 words
17 minutes
装饰器到底是什么:从 Python 解释器的执行模型讲起

大多数装饰器教程讲的是”怎么写”——手动包装、@ 语法糖、*args/**kwargsfunctools.wraps。 这些都对,但如果只停留在这一层,你会知道怎么写装饰器,却不知道 Python 为什么能这样运行。 这篇文章反过来讲:先搞清楚解释器在背后做了什么,再回头看那些语法,你会发现它们全都是水到渠成的结果,而不是需要死记硬背的规则。

〇、两种视角的区别

装饰器可以从两个层面理解:

代码技巧视角(大多数教程停留在这一层):

函数 → 手动包装 → 高阶函数 → 装饰器 → @ 语法糖

运行机制视角(本文重点):

解释器读取代码 → 创建函数对象 → 绑定名字 → 执行赋值 → 名字指向改变 → 调用时走新的引用链

前者告诉你”这么写就行了”,后者告诉你”为什么这么写是对的”。理解了后者,装饰器、闭包、高阶函数这些概念会一次性打通,而不是各自死记硬背。

我第一次真正理解装饰器,是在刷到一个 B 站视频的时候——《Python 面试必问:装饰器到底是个啥?》,那个视频从解释器怎么看待”定义函数”这件事讲起,几句话就把名字绑定和函数对象的底层逻辑讲透了。本文的核心思路正是受它启发,把我自己消化之后的理解完整写下来。

建议先花十几分钟看完这个视频,再往下读——视频建立直觉,本文帮你夯实每一步的细节。


一、Python 解释器眼中的”定义函数”

先看一段最简单的代码:

def hello():
print("hello")

你以为自己”定义了一个函数”。但站在解释器的角度,这行代码实际做了两件事:

  1. 在内存里创建一个函数对象(包含这段代码的字节码、作用域信息等)
  2. hello 这个名字,绑定到这个函数对象上

用一个简单的示意表示这个绑定关系:

名字 hello ────指向────> [函数对象:包含 print("hello") 这段代码]

关键认知:hello 本身不是函数,它只是一个”指向函数对象的名字”(变量)。 这一点是理解装饰器的地基,必须先立住。

所以当你写:

hello()

解释器实际做的是:

  1. 找到 hello 这个名字,看它当前指向哪个对象
  2. 确认这个对象是”可调用的”(函数对象都是可调用的)
  3. 执行这个对象里保存的代码

调用的从来不是”名字”本身,而是名字当前指向的那个对象。 这句话是理解后面所有内容的钥匙。

二、函数是”一等公民”:名字可以随便换绑定对象

既然 hello 只是一个指向函数对象的名字,那自然可以像操作普通变量一样操作它:

def hello():
print("hello")
x = hello # x 现在也指向同一个函数对象,没有发生"复制"
x() # 输出: hello,等价于 hello()

内存关系变成这样:

名字 hello ────┐
├──> 同一个函数对象
名字 x ────┘

hellox 是两个不同的名字,但指向同一个函数对象。这就是”函数是一等公民”在内存层面的真实含义——函数对象和整数、字符串一样,可以被任意数量的名字引用、可以作为参数传递、可以作为返回值传出。

这就是装饰器能够存在的全部物理基础。 一旦你接受”函数只是一个可以被随意传递的对象”,装饰器的实现思路就不再是魔法,而是顺理成章的结果。


三、手动包装:一步步看解释器做了什么

以一个计时需求为例:

def slow_function():
return "完成"
def timer(func):
def wrapper():
print("开始计时")
result = func()
print("计时结束")
return result
return wrapper
slow_function = timer(slow_function)

最后这行 slow_function = timer(slow_function) 是全文最关键的一行,很多人看不懂它在干什么。我们把解释器的执行过程一步步拆开:

第一步:执行前,slow_function 指向原始函数对象

slow_function ────> [原始函数对象:return "完成"]

第二步:执行 timer(slow_function),把原函数对象作为参数传进 timer

进入 timer 函数体后,局部变量 func 现在也指向了这个原始函数对象:

func(timer内部的局部变量) ────> [原始函数对象:return "完成"]

第三步:在 timer 内部,def wrapper(): ... 这行代码执行——创建一个全新的函数对象

wrapper(timer内部的局部变量) ────> [新函数对象:内部逻辑是 调用func、打印耗时等]

注意,这个新的 wrapper 函数对象,它的代码里”记住”了外部的 func 变量(这就是闭包——内部函数可以访问外部函数的局部变量,即使外部函数已经执行结束)。

第四步:return wrapper,把这个新函数对象返回出去

timer(slow_function) 这个表达式的返回值,就是这个新创建的函数对象。

第五步:slow_function = ...,把返回值重新绑定给 slow_function 这个名字

slow_function ────> [新的 wrapper 函数对象]

而原来的那个”只会返回完成”的函数对象,并没有消失,它依然存在于内存中——只是现在只有 wrapper 内部记住的那个 func 变量还指向它,外部已经没有名字能直接访问到它了。

一句话总结这五步:slow_function = timer(slow_function) 做的事情,是把 slow_function 这个名字从”指向原函数”,改成”指向一个新的、包裹了原函数的 wrapper 函数”。原函数本身毫发无损,只是不再被外部直接引用。


四、@ 到底是什么:解释器帮你做的文本替换

理解了上面这套”名字重新绑定”的机制之后,@ 语法糖就非常直白了。这段代码:

@timer
def slow_function():
return "完成"

解释器实际执行时,会把它当成下面这段代码来处理:

def slow_function():
return "完成"
slow_function = timer(slow_function)

两段代码完全等价@timer 只是”定义函数后立刻用装饰器重新绑定名字”这个固定动作的简写。没有隐藏逻辑,没有特殊魔法——@ 纯粹是解释器提供的一个语法糖,帮你省掉手写赋值那一行。

五、调用装饰后的函数:一条完整的引用链

现在执行:

slow_function()

由于 slow_function 现在指向的是 wrapper,调用链条其实是这样的:

调用 slow_function()
实际执行 wrapper() 的代码
├── print("开始计时")
├── 执行 result = func()
│ │
│ ▼
│ 这里的 func 是 wrapper 通过闭包记住的原始函数对象
│ │
│ ▼
│ 执行原始函数的代码,返回 "完成"
├── print("计时结束")
└── 返回 result(也就是 "完成")

你以为自己调用的是”原来那个简单的函数”,实际上调用的是一层套一层的引用链,最终才落到原函数的代码上。理解这条链路,你就能看懂为什么装饰器可以叠加使用@a @b @c 依次装饰同一个函数,本质是嵌套了三层引用),也能看懂为什么装饰器出问题时,报错的堆栈信息经常显示的是 wrapper 而不是原函数名——因为解释器压根不知道”原函数”这个概念,它只知道现在这个名字指向哪个对象。

六、为什么必须要 *args, **kwargs

上面的 wrapper() 是没有参数的。如果原函数需要参数,会直接报错:

def add(a, b):
return a + b
add = timer(add)
add(1, 2)
# TypeError: wrapper() takes 0 positional arguments but 2 were given

从解释器的角度看这个报错非常合理:add 现在指向的是 wrapper 这个对象,而 wrapper 定义时压根没有声明能接收参数的位置。你传了 1, 2 两个参数进去,但 wrapper 的函数签名里没有地方能装下它们,于是报错。

解决方法是让 wrapper 能接收任意数量、任意类型的参数,再原样转发给内部的 func

def timer(func):
def wrapper(*args, **kwargs):
print("开始计时")
result = func(*args, **kwargs) # 把收到的参数原样转发
print("计时结束")
return result
return wrapper
@timer
def add(a, b):
return a + b
print(add(1, 2)) # 正常工作

*args, **kwargs 不是什么特殊语法糖,它只是告诉解释器:“不管调用者传了什么参数,先照单全收,打包成 args 元组和 kwargs 字典”,然后 func(*args, **kwargs) 再把这个包裹拆开,按原样转发给内部的真实函数。

七、被”偷走”的身份信息:functools.wraps

再看一个容易被忽略的细节:

@timer
def add(a, b):
"""计算两个数之和"""
return a + b
print(add.__name__) # 输出: wrapper ← 不是期望的 'add'
print(add.__doc__) # 输出: None ← docstring也没了

这个现象,用前面讲的”名字绑定”模型可以完美解释:add 现在指向的对象,本来就是 wrapper 这个函数对象,而不是原来那个 add 函数对象。__name____doc__ 这些属性是绑定在具体对象上的,wrapper 对象的这些属性当然显示的是它自己的信息,跟原来的 add 毫无关系——它们本来就是两个不同的对象。

解决办法是用标准库提供的 functools.wraps,把原函数的这些元信息手动”搬”到 wrapper 对象上:

import functools
def timer(func):
@functools.wraps(func) # 把 func 的 __name__、__doc__ 等属性复制到 wrapper 身上
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@timer
def add(a, b):
"""计算两个数之和"""
return a + b
print(add.__name__) # 输出: add ← 正确了
print(add.__doc__) # 输出: 计算两个数之和

这行代码不改变任何执行逻辑,只是修复了”对象身份信息对不上”这个问题,让调试工具、文档生成器、依赖函数名做路由的 Web 框架都能正常工作。

八、进阶:带参数的装饰器,是”多绕一层”的名字绑定

如果你想让装饰器本身也能带参数,比如:

@retry(times=3)
def unstable_task():
...

用前面同样的”名字绑定”思路去拆解,这里其实发生了两次函数调用

@retry(times=3)
def unstable_task(): ...
# 完全等价于:
unstable_task = retry(times=3)(unstable_task)

拆成两步看:

第一步retry(times=3) 先被执行——注意,这一步跟”装饰”没关系,就是一次普通的函数调用,传入参数 times=3,返回值是一个新的函数对象(我们通常叫它 decorator)。

第二步decorator(unstable_task)——把第一步返回的这个函数对象,当成”真正的装饰器”来使用,装饰 unstable_task,过程和前面讲的完全一样(返回 wrapper,重新绑定名字)。

对应的实现,是在原来两层结构的基础上,再套一层用来接收装饰器参数的函数

def retry(times):
"""最外层:接收装饰器自己的参数,返回真正的装饰器"""
def decorator(func):
"""中间层:接收被装饰的函数,返回 wrapper"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""最内层:调用时真正执行的代码"""
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第 {attempt} 次尝试失败: {e}")
raise Exception("重试次数已用尽")
return wrapper
return decorator

三层函数,每一层负责的事情都很单一:

  • retry(times):接收装饰器参数,返回”真正的装饰器”
  • decorator(func):接收被装饰的函数,返回 wrapper(这一层就是我们前面讲了一整篇的标准装饰器逻辑)
  • wrapper(*args, **kwargs):调用时真正执行的代码,能访问外层两层函数留下的所有局部变量(timesfunc)——这是多层闭包的体现

多了一层,但拆开看,每一层的本质都没有超出前面讲过的”函数是对象、名字可以重新绑定”这个模型。

九、完整知识地图

Python 运行模型
函数也是对象(不是名字本身)
名字只是"指向对象"的标签,可以重新绑定
函数对象可以作为参数传递、作为返回值传出
闭包:内部函数能记住外部函数的局部变量
装饰器 = 手动重新绑定名字,让名字指向一个"包了一层"的新函数
├── @ 语法糖:本质是 func = decorator(func) 的简写
├── *args/**kwargs:让 wrapper 能接收任意参数并原样转发
├── functools.wraps:修复 wrapper 对象丢失原函数元信息的问题
└── 带参数装饰器:多绕一层,先返回真正的装饰器,再执行装饰逻辑

装饰器真正难的地方,从来不是 @ 这一行代码的写法,而是要在脑子里建立起”名字只是指向对象的标签,函数对象可以被任意传递和重新绑定”这套运行时模型。一旦这套模型立住了,装饰器、闭包、高阶函数、甚至后面会讲到的元类,都只是这套模型在不同场景下的具体应用,不需要死记硬背。

装饰器到底是什么:从 Python 解释器的执行模型讲起
/posts/2026-8-1/python-装饰器从解释器执行模型讲起/
Author
Atopos
Published at
2026-08-01
License
CC BY-NC-SA 4.0

Some information may be outdated