Django + Vue 学习日志(08Python 闭包和装饰器)
PART08:闭包与装饰器终极拆解
记录时间:2026年8月7日(深夜)
环境:Fedora 44 + Python 3.12.13 + Django 5.2.16
一、今日核心问题溯源
在连续拆解 View.as_view() 源码的过程中,触发了对 Python 高阶函数机制的连环追问:
- 闭包定义:闭包是内嵌函数用了上级函数的相关变量?—— 对,但不够精确。
- 装饰器本质:是对原函数的扩展,把原函数当参数传进去,返回扩展函数赋值给原函数?—— 对,但"赋值"不是"别名"。
@decorator执行时机:是立即执行decorator(func)再赋值,还是只执行一半?- 闭包参数一致性:
as_view(cls)和view(request)参数完全不同,这还是闭包吗? - 定义 vs 调用:
def adder():返回了函数,为什么return x + n没执行? - 闭包形成时机:解释器到底什么时候知道形成了闭包?什么时候保存了上级变量?
as_view()像装饰器:它和装饰器到底是什么关系?
这些问题从"会用"一路深挖到"CPython 实现级别"。
二、关键认知突破
1️⃣ 闭包的本质定义(精确版)
一句话定义:
闭包 = 内嵌函数 + 引用了外层(非全局)作用域的局部变量 + 外层函数返回内嵌函数。
三要素(缺一不可):
| 要素 | 说明 | 反例 |
|---|---|---|
| ① 函数嵌套 | 一个函数定义在另一个函数内部 | 平行定义的两个函数 |
| ② 引用外层局部变量 | 内嵌函数使用了外层函数的局部变量 | 只使用全局变量 |
| ③ 外层返回内嵌函数 | 内嵌函数被返回到外部 | 只在内部调用 |
最小验证:
def outer():
x = 10 # ← 外层局部变量
def inner(): # ← ① 嵌套
print(x) # ← ② 引用外层局部变量
return inner # ← ③ 返回内嵌函数
f = outer()
f() # 输出 10 ✅
2️⃣ 闭包形成的精确时机(CPython 级真相)
结论:闭包在函数对象创建时形成,而非调用时或返回时。
解释器的操作流程:
阶段一:编译阶段(Compile Time)
def outer():
x = 10
def inner():
return x + 1
解释器编译 inner 时:
- 扫描
return x + 1 - 发现
x不在inner的局部作用域 - 向上查找,在
outer的局部作用域找到x - 将
x标记为自由变量(Free Variable) - 记录在
inner.__code__.co_freevars中
inner.__code__.co_freevars # ('x',)
阶段二:函数对象创建(Object Creation)
当执行到 def inner: 语句时:
- Python 在当前作用域找到
x = 10 - 将
x封装进一个cell对象 - 把
cell塞进inner.__closure__ - 此时闭包已经完整形成
inner.__closure__ # (<cell at 0x...: int object at 0x...>,)
inner.__closure__[0].cell_contents # 10
阶段三:返回阶段(Return)
return inner
- 只是把已经带闭包的函数对象交出去
- 不创建闭包,不分析变量
阶段四:调用阶段(Call)
f = outer()
f() # 使用闭包中的 x,计算 10 + 1
- 只消费闭包,不生产闭包
关键心法:闭包不是"运行时拼出来的",而是"定义时就封好的"。
cell是闭包的容器,__closure__是闭包的身份证。
3️⃣ 装饰器本质:名字重绑定(非复制、非别名)
一句话定义:
装饰器 = 把原函数作为参数传入装饰器函数,装饰器返回一个新的包装函数,并将这个新函数重新绑定到原函数的名字上。
等价于:
@my_decorator
def hello():
print("hello")
def hello():
print("hello")
hello = my_decorator(hello) # ← 这就是 @ 的全部秘密
逐字校准:
| 你的直觉 | 精确表述 |
|---|---|
| "对原函数的扩展" | ✅ 正确,包装函数增强了原函数 |
| "原函数作为参数传进去" | ✅ 正确,decorator(func) |
| "返回扩展功能的函数" | ✅ 正确,返回 wrapper |
| "复制给了原函数" | ❌ 不准确,是重新绑定 |
| "改成扩展函数的别名" | ❌ 不准确,hello is wrapper 为 False |
内存变化:
装饰前:
hello ──→ <function hello 原始函数>
装饰后:
hello ──→ <function wrapper>
↓
wrapper 内部持有 func ──→ <function hello 原始函数>
✅ 原函数对象还活着(被 wrapper 的闭包引用着)
✅ 只是没人通过 hello 这个名字直接访问它了
✅ 这不是复制,不是别名,是偷梁换柱
4️⃣ @decorator 的执行模型:执行 + 赋值(原子操作)
结论:@decorator 不是语法糖的一半,而是"完整的一行代码"。
Python 官方定义:
@decorator
def func():
...
完全等价于:
def func():
...
func = decorator(func)
执行顺序铁证:
def decorator(func):
print("1. decorator 收到:", func.__name__)
def wrapper():
print("3. wrapper 执行")
func()
print("2. decorator 返回 wrapper")
return wrapper
@decorator
def hello():
print("4. hello 执行")
print("5. hello name:", hello.__name__)
hello()
输出:
1. decorator 收到: hello
2. decorator 返回 wrapper
5. hello name: wrapper
3. wrapper 执行
4. hello 执行
✅ 1→2 发生在 @decorator 那一行(模块加载时)
✅ 5 发生在 print 时
✅ 3→4 发生在 hello() 时(请求到来时)
5️⃣ 闭包参数不一致:职责分离(为什么 as_view 和 view 参数不同)
问题:一般来说闭包的内嵌函数应该和上级函数参数一致,为什么 as_view(cls) 和 view(request) 不一致?
结论:闭包不要求内外函数参数一致。参数一致是装饰器的特征(为了伪装),参数不一致是闭包工厂的特征(因为阶段不同)。
职责分离表:
| 函数 | 调用时机 | 参数 | 参数来源 | 职责 |
|---|---|---|---|---|
as_view(cls, **initkwargs) |
Django 启动 | cls, initkwargs |
程序员写在 urls.py |
工厂:生产视图函数 |
view(request, *args, **kwargs) |
HTTP 请求到来 | request, args, kwargs |
Django 核心传入 | 工人:处理请求 |
类比:
def make_adder(n): # 工厂阶段:配置
def adder(x): # 使用阶段:数据
return x + n
return adder
add5 = make_adder(5) # n = 5 被封存
add5(10) # x = 10,使用闭包中的 n
| make_adder | as_view |
|---|---|
n |
cls, initkwargs |
| 工厂函数 | 类方法工厂 |
返回 adder |
返回 view |
| add5 / adder | view |
|---|---|
记住 n = 5 |
记住 cls, initkwargs |
调用时接收 x |
调用时接收 request |
| 闭包 | 闭包 |
关键心法:装饰器是"伪装成原函数"(参数一致),闭包工厂是"造一个新函数"(参数不同)。
as_view()是工厂,不是装饰器。
6️⃣ 定义 vs 调用:写菜谱 vs 炒菜(公理级认知)
结论:函数定义只绑定对象,函数调用才执行逻辑。 这是 Python 语义的一条公理,没有任何例外。
make_adder 逐帧拆解:
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add5(10) # 15
完整时间轴:
| 顺序 | 事件 | 创建对象 | 执行函数体 |
|---|---|---|---|
| 1 | 定义 make_adder |
✅ | ❌ |
| 2 | 调用 make_adder(5) |
✅ | ✅ |
| 3 | 创建局部变量 n = 5 |
✅ | - |
| 4 | 定义 adder(闭包形成) |
✅ | ❌ |
| 5 | 返回 adder |
- | - |
| 6 | add5 = adder(绑定) |
✅ | ❌ |
| 7 | 调用 add5(10) |
✅(栈帧) | ✅ |
| 8 | 查找 x = 10(局部) |
- | - |
| 9 | 查找 n = 5(闭包) |
- | - |
| 10 | 返回 15 |
- | ✅ |
核心确认:
def make_adder(n):
def adder(x):
return x + n
# 在 return 之前检查
print("自由变量:", adder.__code__.co_freevars) # ('n',)
print("闭包内容:", adder.__closure__) # (<cell: 5>,)
return adder
make_adder(5)
✅ 闭包在 return 之前已经完整形成
✅ def adder: 只是写菜谱,adder() 才是炒菜
7️⃣ URLConf 中的 as_view():返回函数,不调用函数
代码:
path('register/', views.RegisterView.as_view(), name='register'),
逐字分析:
views.RegisterView.as_view() # ← 调用 as_view(),返回 view 函数对象
✅ 返回的是 view 函数对象
❌ 不是 view() 的调用结果
✅ URLConf 只负责"登记"这个可调用对象
✅ 真正的调用发生在 HTTP 请求到来时
铁证实验:
print(views.RegisterView.as_view()) # <function View.as_view.<locals>.view at 0x...>
print(type(views.RegisterView.as_view())) # <class 'function'>
✅ 函数对象
❌ 不是 HttpResponse
❌ 不是 None
和装饰器的完美对照:
| 对比项 | 装饰器 | as_view() |
|---|---|---|
| 输入 | 原函数 func |
类 cls |
| 输出 | 包装函数 wrapper |
新函数 view |
| 是否替换名字 | ✅ hello = wrapper |
❌ 不涉及 |
| 是否闭包 | ✅ 是 | ✅ 是 |
| 是否"偷梁换柱" | ✅ 是 | ❌ 是"造新房" |
| 目的 | 增强现有函数 | 类转函数 |
三、闭包的核心作用(全景总结)
作用 1:封装私有变量(状态保持)
def counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = counter()
print(c()) # 1
print(c()) # 2
✅ count 对外不可见,只能通过 inc() 修改。
作用 2:装饰器(功能增强)
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"耗时: {time.time() - start:.2f}s")
return result
return wrapper
✅ wrapper 记住了 func,离开原函数作用域后仍能调用。
作用 3:函数工厂(动态生成函数)
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
✅ 生成行为相似但参数不同的函数。
作用 4:延迟执行(回调 / 中间件)
def lazy_sum(a, b):
def inner():
return a + b
return inner
s = lazy_sum(1, 2)
result = s() # 此刻才计算
作用 5:替代全局变量(工程最佳实践)
# ❌ 全局变量(危险)
count = 0
def inc():
global count
count += 1
# ✅ 闭包(安全)
def counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
Django 中的闭包全家福
| Django 组件 | 闭包作用 |
|---|---|
View.as_view() |
冻结 cls + initkwargs |
method_decorator |
适配函数装饰器到方法 |
| 中间件 | 记住 get_response |
| Signals | 记住回调函数 |
| ORM QuerySet | 延迟 SQL 执行 |
| DRF GenericAPIView | 冻结 queryset / serializer_class |
四、代码验证(铁证级实验集合)
实验 1:验证闭包在返回前已形成
def make_adder(n):
def adder(x):
return x + n
print("自由变量:", adder.__code__.co_freevars)
print("闭包内容:", adder.__closure__)
return adder
make_adder(5)
输出:
自由变量: ('n',)
闭包内容: (<cell at 0x...: int object at 0x...>,)
✅ 闭包在 return 之前已经完整。
实验 2:验证装饰器的名字重绑定
def decorator(func):
def wrapper():
print("wrapper calling")
func()
return wrapper
@decorator
def hello():
print("hello")
print("hello.__name__:", hello.__name__)
print("hello is wrapper:", hello is wrapper if 'wrapper' in dir() else 'N/A')
hello()
输出:
hello.__name__: wrapper
wrapper calling
hello
✅ hello 这个名字已经指向 wrapper。
实验 3:验证 @decorator 是立即执行
def my_dec(func):
print(f"装饰器立即执行,收到: {func.__name__}")
def wrapper():
print("wrapper 执行")
func()
return wrapper
@my_dec
def test():
print("test 执行")
print("--- 到这里,test 已经被装饰了 ---")
test()
输出:
装饰器立即执行,收到: test
--- 到这里,test 已经被装饰了 ---
wrapper 执行
test 执行
✅ @my_dec 在模块加载时立即执行,不是延迟执行。
实验 4:验证 def 不执行函数体
def make_adder(n):
print(f"make_adder 被调用,n = {n}")
def adder(x):
print(f"adder 被调用,x = {x}")
return x + n
print("make_adder 即将返回 adder")
return adder
print("开始")
add5 = make_adder(5)
print("add5 已赋值")
print("准备调用 add5")
result = add5(10)
print("结果:", result)
输出:
开始
make_adder 被调用,n = 5
make_adder 即将返回 adder
add5 已赋值
准备调用 add5
adder 被调用,x = 10
结果: 15
✅ adder 被调用 出现在 add5 已赋值 之后。
✅ def adder: 时 return x + n 没有执行。
五、与 Django CBV 的终极串联
View 类最小教学版(带闭包标注)
class View:
http_method_names = ['get', 'post', 'put', 'delete']
@classmethod
def as_view(cls, **initkwargs):
"""类方法工厂 + 闭包"""
def view(request, *args, **kwargs): # ← 闭包函数
self = cls(**initkwargs) # ← 使用闭包变量 cls
self.setup(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)
view.cls = cls
view.initkwargs = initkwargs
return view # ← 返回闭包函数对象
def setup(self, request, *args, **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
def dispatch(self, request, *args, **kwargs):
method = request.method.lower()
handler = getattr(self, method) # ← 返回方法对象
return handler(request, *args, **kwargs) # ← 加 () 才调用
def get(self, request, *args, **kwargs):
raise NotImplementedError
完整调用链(从 URL 到 Response)
┌─────────────────────────────────────────────────────────┐
│ 模块加载阶段(Django 启动) │
├─────────────────────────────────────────────────────────┤
│ 1. 执行 RegisterView.as_view() │
│ 2. 创建 view 函数(闭包形成,封存 cls + initkwargs) │
│ 3. 返回 view 函数对象 │
│ 4. URLConf: path('register/', view) │
├─────────────────────────────────────────────────────────┤
│ HTTP 请求到来 │
├─────────────────────────────────────────────────────────┤
│ 5. Django 核心调用 view(request) │
│ 6. view 中: self = cls(**initkwargs) ← 创建实例 │
│ 7. self.setup(request) ← 绑定 request │
│ 8. self.dispatch(request) ← 调度 │
│ 9. getattr(self, 'get') ← 拿到方法对象 │
│ 10. handler(request) ← 调用 get() │
│ 11. 返回 HttpResponse │
└─────────────────────────────────────────────────────────┘
六、常见误区澄清
| 误区 | 正解 |
|---|---|
| 闭包是"只要嵌套" | 必须引用外层局部变量,全局变量不算 |
| 闭包内外函数参数要一致 | 装饰器倾向一致(伪装),工厂倾向不同(解耦) |
@decorator 只是标记 |
是完整的 func = decorator(func) 执行+赋值 |
| 装饰器是复制或别名 | 是名字重绑定,hello is wrapper 为 False |
def adder: 会执行 return x + n |
def 只创建对象,调用 adder() 才执行 |
| 闭包在调用时才形成 | 编译时标记自由变量,创建函数对象时封装 cell |
as_view() 是装饰器 |
是闭包工厂,和装饰器是"亲兄弟"但不是同一个 |
URLConf 调用了 view() |
URLConf 只存函数引用,Django 核心在请求时才调用 |
getattr(self, 'get') 调用了 get |
返回方法对象,加 () 才是调用 |
七、今日金句集
闭包:"带着出生环境逃跑的函数。环境不丢,记忆永在。"
装饰器:"偷梁换柱——名字没变,背后的函数已经换了。"
@decorator:"不是标记,是执行语句;先调用,后换绑,一气呵成。"
定义 vs 调用:"def 是写菜谱,函数名() 是炒菜;菜谱写得再详细,不点餐就不会出锅。"
闭包参数不一致:"工厂参数和产品参数无需雷同。造枪的不需要和被枪击中的人姿势一样。"
as_view()vs 装饰器:"装饰器是改装旧车,as_view() 是按图纸造新车。发动机一样(闭包),产品不同。"

浙公网安备 33010602011771号