Django + Vue 学习日志(08Python 闭包和装饰器)

PART08:闭包与装饰器终极拆解

记录时间:2026年8月7日(深夜)
环境:Fedora 44 + Python 3.12.13 + Django 5.2.16


一、今日核心问题溯源

在连续拆解 View.as_view() 源码的过程中,触发了对 Python 高阶函数机制的连环追问:

  1. 闭包定义:闭包是内嵌函数用了上级函数的相关变量?—— 对,但不够精确。
  2. 装饰器本质:是对原函数的扩展,把原函数当参数传进去,返回扩展函数赋值给原函数?—— 对,但"赋值"不是"别名"。
  3. @decorator 执行时机:是立即执行 decorator(func) 再赋值,还是只执行一半?
  4. 闭包参数一致性as_view(cls)view(request) 参数完全不同,这还是闭包吗?
  5. 定义 vs 调用def adder(): 返回了函数,为什么 return x + n 没执行?
  6. 闭包形成时机:解释器到底什么时候知道形成了闭包?什么时候保存了上级变量?
  7. 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: 语句时:

  1. Python 在当前作用域找到 x = 10
  2. x 封装进一个 cell 对象
  3. cell 塞进 inner.__closure__
  4. 此时闭包已经完整形成
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 wrapperFalse

内存变化

装饰前:
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_viewview 参数不同)

问题:一般来说闭包的内嵌函数应该和上级函数参数一致,为什么 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() 是按图纸造新车。发动机一样(闭包),产品不同。"


posted @ 2026-08-08 23:54  YiYezc  阅读(5)  评论(0)    收藏  举报