文章目录
- [Python 装饰器从入门到源码(上)------闭包、自由变量与你的第一个装饰器](#Python 装饰器从入门到源码(上)——闭包、自由变量与你的第一个装饰器)
-
- 导入语
- [1 ~> 函数是一等公民------Python 和 Java 的关键差异](#1 ~> 函数是一等公民——Python 和 Java 的关键差异)
-
- [1.1 函数可以赋值给变量](#1.1 函数可以赋值给变量)
- [1.2 函数可以作为参数传递](#1.2 函数可以作为参数传递)
- [1.3 函数内部可以定义函数](#1.3 函数内部可以定义函数)
- [2 ~> 闭包------内部函数"记住"了外部的变量](#2 ~> 闭包——内部函数"记住"了外部的变量)
-
- [2.1 闭包长什么样](#2.1 闭包长什么样)
- [2.2 为什么"记住"了?------自由变量](#2.2 为什么"记住"了?——自由变量)
- [2.3 闭包的自由变量存在哪------`closure`](#2.3 闭包的自由变量存在哪——
__closure__)
- [3 ~> 闭包的经典陷阱------循环中的自由变量](#3 ~> 闭包的经典陷阱——循环中的自由变量)
-
- [3.1 现象](#3.1 现象)
- [3.2 原因分析](#3.2 原因分析)
- [3.3 修复------用默认参数"固化"值](#3.3 修复——用默认参数"固化"值)
- [4 ~> `nonlocal`------在闭包里修改自由变量](#4 ~>
nonlocal——在闭包里修改自由变量) -
- [4.1 问题](#4.1 问题)
- [4.2 解决方案](#4.2 解决方案)
- [5 ~> 手写第一个装饰器------计时器](#5 ~> 手写第一个装饰器——计时器)
- [思考 && 总结](#思考 && 总结)
- 结尾
Python 装饰器从入门到源码(上)------闭包、自由变量与你的第一个装饰器
📖 文章简介: 装饰器是 Python 面试和进阶的经典分水岭。上篇聚焦装饰器的前置知识------闭包与自由变量。从"函数是一等公民"讲起,逐步推导:函数可以作为参数传递 → 函数内可以定义函数 → 内部函数可以捕获外部函数的局部变量(闭包) → 闭包的自由变量到底存在哪里 → nonlocal 的作用。最后手写第一个装饰器,用计时器案例展示装饰器如何在不修改原函数的前提下增加新功能。每一步都配 __code__、__closure__ 等属性验证,让你看清闭包的内部结构。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
装饰器------Python 面试的高频考点,也是很多人学了又忘、忘了又学的一个东西。说实话,我当初学到装饰器的时候,看到 @app.route() 和 @staticmethod 就觉得这是某种"黑魔法"。直到后来把闭包彻底搞懂,才发现装饰器根本不是魔法------它就是闭包的一个具体应用而已。
上篇的任务是帮你建立理解装饰器所需的前置知识:闭包。很多人觉得装饰器难的原因就是跳过了闭包直接背语法------这就跟跳过了乘法直接背解方程一样,能解题但不知道每一步什么意思。我们把闭包拆透,下篇再上装饰器进阶(带参数的装饰器、@wraps),就顺理成章了。
1 ~> 函数是一等公民------Python 和 Java 的关键差异
1.1 函数可以赋值给变量
python
def greet(name):
return f"Hello, {name}"
say = greet # 把函数赋值给变量(不是调用!没有括号)
print(say("世界")) # Hello, 世界
print(say is greet) # True,同一个函数对象
1.2 函数可以作为参数传递
python
def apply(func, value):
return func(value)
def double(x):
return x * 2
print(apply(double, 5)) # 输出:10
1.3 函数内部可以定义函数
python
def outer():
def inner(): # 在函数内部定义另一个函数
print("我是内部函数")
return inner # 把内部函数作为返回值返回
f = outer() # f 现在是一个函数
f() # 输出:我是内部函数
这三步------赋值、传参、内部定义------合起来叫"函数是一等公民"。Java 里要实现类似效果需要匿名内部类或者 Lambda。Python 在语法层面就支持。装饰器就是在这三个特性之上构建的。
2 ~> 闭包------内部函数"记住"了外部的变量
2.1 闭包长什么样
python
def make_multiplier(n):
def multiplier(x):
return x * n # n 是外层函数 make_multiplier 的参数
return multiplier
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)
print(times_3(10)) # 输出:30
print(times_5(10)) # 输出:50
times_3 和 times_5 是两个独立的"乘法器",它们各自"记住"了创建时的 n 值。 这就是闭包。
2.2 为什么"记住"了?------自由变量
在 multiplier 函数里,n 不是 multiplier 的局部变量,也不是全局变量------它来自于外层函数 make_multiplier。这种"不是自己定义、也不在全局、来自外层作用域"的变量叫自由变量(free variable)。
2.3 闭包的自由变量存在哪------__closure__
python
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_3 = make_multiplier(3)
print(times_3.__closure__) # 有一个 cell 对象
print(times_3.__closure__[0].cell_contents) # 输出:3 ← 自由变量 n 的值
__closure__ 是个元组,装着一组 cell 对象,每个 cell 保存了一个自由变量的引用 。cell_contents 属性就是自由变量当前的值。
闭包之所以能"记住"外部变量,是因为 Python 偷偷把这些变量装进了函数对象的 __closure__ 属性里。即使外部函数已经返回了,这些变量被 cell 对象引用着,不会回收。
3 ~> 闭包的经典陷阱------循环中的自由变量
3.1 现象
python
funcs = []
for i in range(3):
funcs.append(lambda: i) # 三个 lambda,各自应该打印 0, 1, 2?
for f in funcs:
print(f()) # 输出:2 2 2 ← 全是 2!
3.2 原因分析
三个 lambda 都引用的是同一个自由变量 i 。循环结束时 i 的值是 2,所以三个 lambda 调用时都输出 2。这不是"lambda 没记住",而是"它们记住的是同一个变量 i 的引用,而不是各自拷贝了一份值"。
3.3 修复------用默认参数"固化"值
python
funcs = []
for i in range(3):
funcs.append(lambda x=i: x) # 默认参数在定义时就确定了值
for f in funcs:
print(f()) # 输出:0 1 2 ✓
默认参数的值在函数定义时就计算好了------相当于一个快照。而自由变量一直跟着引用走------只能看到最新的值。这个区别在面试里考了无数遍。
4 ~> nonlocal------在闭包里修改自由变量
4.1 问题
python
def counter():
count = 0
def increment():
count += 1 # ❌ UnboundLocalError
return count
return increment
为什么报错?原因和前面讲的一样------count += 1 等价于 count = count + 1,在 Python 看来这是赋值 操作,Python 就把 count 标记为 increment 的局部变量。但 count + 1 时局部变量 count 还没赋过值,于是 UnboundLocalError。
4.2 解决方案
python
def counter():
count = 0
def increment():
nonlocal count # 声明:count 不是我的局部变量,用外层那个
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2 ← 每次调用都累加
nonlocal 告诉 Python:这个变量别当局部变量处理,顺着作用域链往外找。找到后可以读写它。
5 ~> 手写第一个装饰器------计时器
把前面学到的串起来,写一个计时装饰器:
python
import time
def timer(func): # ① 接收一个函数
def wrapper(*args, **kwargs): # ② 把原函数"包一层"
start = time.perf_counter()
result = func(*args, **kwargs) # ③ 调用原函数
end = time.perf_counter()
print(f"{func.__name__} 耗时:{end - start:.6f}秒")
return result # ④ 返回原函数的返回值
return wrapper # ⑤ 返回包装后的函数
@timer # ← 装饰器语法
def slow_function():
total = 0
for i in range(10_000_000):
total += i
return total
result = slow_function()
# 输出:slow_function 耗时:0.382041秒
@timer 等价于:
python
slow_function = timer(slow_function)
从内到外看:timer(slow_function) 返回 wrapper,wrapper 包装了原始函数,加了计时逻辑。从此每次调用 slow_function(),实际执行的是 wrapper()。
思考 && 总结
上篇的核心------两条地基:
- 闭包 = 内部函数 + 自由变量存储在
__closure__。 闭包让内部函数"记住"创建时的外部变量值,而不是每次现场计算。__closure__属性中的cell对象就是闭包的数据储存单元。 - 装饰器 = 闭包 + 函数是一等公民。
@decorator是把目标函数作为参数传给装饰器函数,装饰器返回一个包装后的新函数。装饰器的本质就是:在不修改原函数的前提下,给函数添加新功能。
下篇深入装饰器进阶------带参数的装饰器是什么原理、functools.wraps 为什么不能省、以及装饰器在实际项目(Django / Flask / 权限校验)中的真实写法。
结尾
各位小伙伴,上篇到此结束,感谢阅读!
源码骑士 --- Python 全栈 & 系统架构
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!
🗡️ 寄语:技术之路,同行的人会让前路更有方向
结语:闭包是理解装饰器的大门,这扇门现在开了。下篇见,一键四连别忘了!