06-Python装饰器从入门到源码(上)-闭包与自由变量

文章目录

  • [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_3times_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) 返回 wrapperwrapper 包装了原始函数,加了计时逻辑。从此每次调用 slow_function(),实际执行的是 wrapper()


思考 && 总结

上篇的核心------两条地基:

  1. 闭包 = 内部函数 + 自由变量存储在 __closure__ 闭包让内部函数"记住"创建时的外部变量值,而不是每次现场计算。__closure__ 属性中的 cell 对象就是闭包的数据储存单元。
  2. 装饰器 = 闭包 + 函数是一等公民。 @decorator 是把目标函数作为参数传给装饰器函数,装饰器返回一个包装后的新函数。装饰器的本质就是:在不修改原函数的前提下,给函数添加新功能。

下篇深入装饰器进阶------带参数的装饰器是什么原理、functools.wraps 为什么不能省、以及装饰器在实际项目(Django / Flask / 权限校验)中的真实写法。


结尾

各位小伙伴,上篇到此结束,感谢阅读!

源码骑士 --- Python 全栈 & 系统架构

👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长

❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量

收藏:把核心知识点存好,在需要时随时查、随时用

💬 评论:分享你的经验或疑问,评论区一起交流避坑

🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!

🗡️ 寄语:技术之路,同行的人会让前路更有方向

结语:闭包是理解装饰器的大门,这扇门现在开了。下篇见,一键四连别忘了!

相关推荐
码云骑士1 小时前
10-Python运行时内存模型-栈帧-堆-引用计数-GC分代回收的全景图
开发语言·python
智码看视界1 小时前
老梁聊全栈系列 JavaScript语言本质:从原型链到异步编程的深度解析
开发语言·javascript·全栈·javascript核心
AI科技星2 小时前
数术工坊・八卷全书【本源创世终极版・万世定稿】
开发语言·网络·量子计算·拓扑学
雾沉川2 小时前
Visual C++ 运行库合集 v105.0 部署与故障排查技术指南
开发语言·c++·dll
码云骑士2 小时前
02-Python可变对象与不可变对象(上)-赋值陷阱与函数传参的暗坑
开发语言·python
疯狂学习GIS2 小时前
基于Python earthaccess库批量下载全球MODIS GPP(MOD17A2HGF)数据
python·脚本·批量下载·遥感影像·nasa·earthdata·自动处理
至乐活着2 小时前
用DeepSeek打造你自己的智能问答系统:从零到一的完整指南
python·deepseek·ai应用开发·智能问答系统·api教程
AI创界者2 小时前
【解压即用】Scail-2 视频动作迁移一键整合包:8G显存通吃50系,长视频/多人/精准目标替换全攻略
人工智能·python·aigc·音视频