文章目录Python 变量赋值的底层真相你写的 a 1 到底发生了什么导入语1 ~ 变量不是盒子变量是标签1.1 现象1.2 原因分析1.3 底层原理1.4 验证用 id() 查看内存地址2 ~ a 1 和 b 1 之后a 和 b 指向同一个 1 吗2.1 现象2.2 原因分析2.3 验证小整数池的边界2.4 解决方案3 ~ 不可变对象赋值时究竟不变在哪里3.1 现象3.2 原因分析3.3 核心对比3.4 用 id() 验证4 ~ 函数传参Python 到底是值传递还是引用传递4.1 现象4.2 原因分析4.3 一个更直观的类比4.4 变量说明5 ~ 字符串的驻留机制——你以为是同一个Python 也觉得是5.1 现象5.2 原因分析5.3 字符串驻留有什么卵用6 ~ is vs ——你以为在比大小其实在比地址6.1 现象6.2 原因分析6.3 什么时候用 is7 ~ 引用计数——Python 怎么知道什么时候该删掉一个对象7.1 现象7.2 原因分析7.3 一个容易被忽略的细节8 ~ 循环引用当两个对象互相指引用计数就失效了8.1 现象8.2 原因分析8.3 实战中的循环引用9 ~ 变量声明在 Python 里是不存在的9.1 现象9.2 原因分析9.3 这带来的一个常见坑思考 总结结尾Python 变量赋值的底层真相你写的a 1到底发生了什么文章简介大部分Python教程第一课就教变量赋值但很少有人把a 1背后发生了什么讲清楚。本文从CPython源码视角拆解Python变量赋值的底层机制变量不是盒子、赋值不是拷贝、内存里到底发生了什么。文章采用现象→原因→原理三段式讲解覆盖引用计数、小整数池、字符串驻留、is vs 的内存级区别、函数传参是传引用还是传值这个经典面试题、以及可变对象与不可变对象在赋值时的行为差异。文中每一组结论都配了可复现的代码验证读完你会彻底明白为什么别人说Python里一切皆对象、变量只是标签。 个人主页源码骑士❄专栏传送门《Android开发基础》《python基础课程》⭐️热衷从源码视角拆解技术底层原理将复杂架构讲得通俗易懂 源码骑士的简介5年Android Framework系统开发经验曾主导多项系统级性能优化专项技术栈覆盖Android系统全链路Binder/Handler/AMS/WMS/启动流程及Java后端全家桶Spring MyBatis Redis Oracle累计产出原创技术文章100篇文章以源码拆解为特色被读者评价为看一篇胜过啃一周文档导入语如果你写过Python你一定写过这行代码a1如果有人问你这行代码在内存里发生了什么你能讲清楚吗我面试过不少写了两三年Python的人问到这个问题时答案惊人的统一“变量a指向了整数对象1”。然后——就没有然后了。再追问 “1 这个对象放在内存哪里a 怎么找到它的b 1之后 a 和 b 是不是指向同一个 1” 对面就开始支支吾吾。这篇文章的目的很单纯把 Python 变量赋值背后的内存机制用现象 → 原因 → 原理的方式一层层拆开。每一个结论都配了你可以自己跑通的验证代码看完之后你对 Python 变量的理解会和以前完全不一样。1 ~ 变量不是盒子变量是标签1.1 现象先用一段代码来挑战一下直觉a[1,2,3]ba b.append(4)print(a)# 输出[1, 2, 3, 4]你明明改的是b为什么a也跟着变了1.2 原因分析Java/C 程序员容易把变量想象成盒子——int a 1就是往 a 这个盒子里放进一个整数 1。但Python不是这样。在 Python 里变量根本不是盒子。变量只是贴在对象身上的一张标签。a [1, 2, 3] → 创建一个列表对象在上面贴了张标签叫 a b a → 同一张列表对象上又贴了张标签叫 b b.append(4) → 通过标签 b 找到这个对象往里面加了 4 → a 和 b 指向同一个对象当然 a 也变了1.3 底层原理用一张内存图来理解Python 堆内存 ┌─────────────────────┐ │ list 对象 │ │[1,2,3]← a │ ← a 和 b 都指向这里 │ ← b │ │ │ │ b.append(4)之后 │ │[1,2,3,4]│ ← 同一个对象内容变了 └─────────────────────┘1.4 验证用id()查看内存地址a[1,2,3]baprint(id(a))# 输出2773139414208你的数字可能不同print(id(b))# 输出2773139414208和上面一样print(aisb)# 输出Trueid(a)和id(b)完全一样——证明了 a 和 b 确实是指向同一个内存地址。一句话结论Python 的赋值语句a xxx翻译过来的意思是把标签 a 贴到对象 xxx 上而不是把 xxx 拷贝到 a 这个盒子里。这个认知是理解后面所有内容的基石。2 ~a 1和b 1之后a 和 b 指向同一个 1 吗2.1 现象a1b1print(aisb)# 输出True ← 嗯不是两个不同的 1换成一个大一点的数字呢a300b300print(aisb)# 输出False ← 这次又是不同的了同一个赋值逻辑结果却不一样。这是 Python 最让人困惑的行为之一。2.2 原因分析Python 在启动时预先创建了一小批常用整数对象-5 到 256。这个小整数池里的整数全局只有一份不会重复创建。所以a 1——Python 直接从小整数池里拿出现成的 1 对象贴上标签 a。b 1——Python 又去找 1发现池里已经有了直接把标签 b 也贴上去。结果a 和 b 指向同一个 1a is b为True。而 300 不在小整数池范围内每次 300都会新建一个整数对象所以a is b为False。2.3 验证小整数池的边界# 边界测试print(-5is-5)# True 池内print(-5is-61)# True -5在池内-61 算出 -5还是从池里拿print(256is256)# True 池内print(257is257)# False 池外注意这是 CPython 的行为如果在同一个表达式里用两个 257 呢a257;b257print(aisb)# False分别创建不是同一个对象# 但如果在同一行print(257is257)# True ← 为什么同一行的 257 is 257 又变 True 了因为 CPython 编译器看到同一行有相同的不可变常量会做优化——直接共享同一个对象。这不是小整数池的功劳而是编译期常量折叠。2.4 解决方案你想干什么用哪个判断值是否相等判断是不是同一个对象is几乎只用来和None比较日常写代码用就够了。is只在需要判断是不是同一个对象时用比如if x is None。不要拿is去比较数字和字符串的值你以为在比大小其实在比地址。3 ~ 不可变对象赋值时究竟不变在哪里3.1 现象ahelloba aa worldprint(a)# 输出hello worldprint(b)# 输出hello没变同样是b a列表的例子中 b 跟着变了字符串的例子中 b 却没变。为什么3.2 原因分析字符串是不可变对象。a a world不是在原地修改原来的字符串而是创建了一个全新的字符串对象然后把标签 a 从旧对象上撕下来贴到新对象上。整个过程分三步1. ahello→ 在内存中创建一个 str 对象hello贴上标签 a2. ba → 同一个hello对象上再贴个标签 b3. aa world→ ① 先创建一个全新的 str 对象hello world→ ② 把标签 a 从旧对象hello上撕下来 → ③ 把标签 a 贴到新对象hello world上 → ④ 标签 b 还贴在原来的hello上 → b 不变3.3 核心对比可变对象list/dict/set不可变对象int/str/tuple修改内容原地修改所有标签都能看到变化无法原地修改修改 创建新对象b a后修改 ab 跟着变同一个对象b 不变a 的标签被贴到了新对象上内存变化修改前后是同一个地址每次修改都是新地址3.4 用id()验证ahelloprint(id(a))# 地址 Aaa worldprint(id(a))# 地址 B和地址 A 不同baprint(id(b))# 地址 B和 a 一样每行id()的输出值都不同说明每次修改字符串都在创建新对象。一句话总结列表中 b 跟着变是因为 a 和 b 看着同一个箱子箱子里东西被动过了。字符串中 b 不跟着变是因为箱子本身被换掉了——a 搬到了新箱子b 还留在旧箱子门口。4 ~ 函数传参Python 到底是值传递还是引用传递4.1 现象面试高频题。先看代码# 传不可变对象defchange_int(x):x200a100change_int(a)print(a)# 输出100没变# 传可变对象defchange_list(lst):lst.append(4)b[1,2,3]change_list(b)print(b)# 输出[1, 2, 3, 4]变了同一个函数传参机制传 int 没变传 list 变了。这到底是传值还是传引用4.2 原因分析Python 的传参既不是纯粹的传值也不是纯粹的传引用。准确的说法是传对象引用Pass by Object Reference。具体解释change_int(a)的执行过程1. 函数调用时形参 xa → x 也贴到了整数对象100上2. 执行 x200→ x 从100上撕下来贴到新对象200上3. 函数结束 → a 的标签从来没被撕过还是指向100change_list(b)的执行过程1. 函数调用时形参 lstb → lst 也贴到了列表对象[1,2,3]上2. 执行 lst.append(4)→ 通过 lst 找到列表对象原地修改3. 函数结束 → b 还是指向同一个列表对象而对象内容已经变了4.3 一个更直观的类比把 Python 的变量想象成便签贴纸传参就是把便签纸的复印件递给函数——复印件上写着同一个地址。如果你通过复印件找到房子把房子里的东西改了修改可变对象——原版便签纸上的地址没变但房子里的东西确实变了。如果你在复印件上重新写一个新地址对不可变对象重新赋值——原版便签纸不受影响。4.4 变量说明说法对不对准确说法“Python是值传递”❌ 不完全对int 没变但 list 变了值传递解释不了list的行为“Python是引用传递”❌ 也不对如果你传入 int 后改变它实参不会跟着变“Python传对象引用”✅ 准确形参获得的是对象的引用不是值的拷贝也不是变量本身的引用记住传参时函数内部拿到的是能找到同一个对象的地址。能不能通过这个地址改动原对象取决于对象本身支不支持原地修改可变 vs 不可变。5 ~ 字符串的驻留机制——你以为是同一个Python 也觉得是5.1 现象ahellobhelloprint(aisb)# 输出True ← 和小整数池一模一样的味道# 但这种情况chello worlddhello worldprint(cisd)# 输出True ← 编译器优化# 而如果动态拼出来ehellofhelloprint(eisf)# 输出True ← 这是编译时常量折叠# 真正动态的情况importsys gsys.intern(hello)hsys.intern(hello)print(gish)# 输出True ← intern 强制驻留5.2 原因分析Python 对字符串做了和整数类似的优化叫字符串驻留String Interning。规则如下情况是否会驻留说明编译期确定、只含字母数字下划线✅ 自动驻留符合标识符规范的短字符串编译期常量折叠✅a b在编译时就算出ab了动态拼接、含特殊字符❌ 不自动驻留运行时才确定的字符串用sys.intern()手动驻留✅ 强制驻留你主动要求 Python 缓存这个字符串5.3 字符串驻留有什么卵用Python 代码里到处是字符串——函数名、属性名、字典的 key。如果每出现一个__init__就要新建一份内存浪费不可估量。驻留机制保证相同的短字符串全 Python 进程只有一份字典查找 key 的时候直接用地址比较is速度比逐个字符比较快得多。# 你看不到的地方Python 在做大量驻留classMyClass:def__init__(self):pass# __init__ 这个字符串被驻留了所有类的 __init__ 都共享同一份6 ~isvs——你以为在比大小其实在比地址6.1 现象a[1,2,3]b[1,2,3]print(ab)# 输出True ← 内容相同print(aisb)# 输出False ← 但不是同一个对象cNonedNoneprint(cisd)# 输出True ← None 全局唯一6.2 原因分析操作比较什么中文a ba.__eq__(b)“a 和 b 内容相同吗”a is bid(a) id(b)“a 和 b 是同一个对象吗”调用的是对象的__eq__方法比较的是内容。is比较的是两个变量在内存中的地址是否相同不能被重载。6.3 什么时候用isis几乎只用在两个场景# 场景1判断是否为 None最常用PEP 8 官方推荐ifxisNone:pass# 场景2判断是否为特定单例较少见ifobjissentinel:pass其他一切比大小“比内容”比数值的场景老老实实用。用is去比较两个列表是否内容相同是新手典型错误。7 ~ 引用计数——Python 怎么知道什么时候该删掉一个对象7.1 现象importsys a[1,2,3]print(sys.getrefcount(a))# 输出2a 一个引用getrefcount 的参数临时又多一个baprint(sys.getrefcount(a))# 输出3多了一个引用delbprint(sys.getrefcount(a))# 输出2b 的引用没了dela# list 对象引用计数回到 0被垃圾回收7.2 原因分析Python 每个对象都有一个计数器记录着当前有多少个变量指向它。计数器归零时Python 就知道这个对象没人用了可以回收它占用的内存。a[1,2,3]→ 引用计数1a 指向它 ba → 引用计数2a 和 b 都指向它 del b → 引用计数1b 的引用解除 del a → 引用计数0没人引用了→ 触发垃圾回收7.3 一个容易被忽略的细节sys.getrefcount(a)的结果永远比你预想的多 1——因为把 a 作为参数传递给 getrefcount 这个操作本身也创建了一个临时引用。a42print(sys.getrefcount(a))# 输出可能 11一大堆不完全是 2# 因为 42 是小整数Python 内部很多地方都在引用它8 ~ 循环引用当两个对象互相指引用计数就失效了8.1 现象classNode:def__init__(self,name):self.namename self.refNoneaNode(A)bNode(B)a.refb b.refa# 现在 a 和 b 互相引用deladelb# 这两个 Node 对象还能被回收吗8.2 原因分析引用计数遇到循环引用就彻底歇菜。a 被删了b 对 a 的引用还在所以 a 的引用计数不是 0。同理 b 也不是 0。结果两个对象谁都回收不了——内存泄漏。Python 不是傻子它还有一个**辅助垃圾回收器分代垃圾回收**专门处理这种情况。它隔一段时间扫描所有可能形成环的对象容器类发现循环引用就主动断开回收。8.3 实战中的循环引用# 列表自己引用自己lst[1,2,3]lst.append(lst)# lst[3] 就是 lst 自己dellst# 引用计数不是 0要等 GC 来救好在 Python 的 GC 默认是打开的日常写代码基本不用操心。但如果你在性能敏感的场景大量创建容器对象又解开引用这个知识能帮你理解为什么内存不会立刻释放。9 ~ 变量声明在 Python 里是不存在的9.1 现象Java 程序员转 Python 最常见的一个习惯inta;# ❌ Python 没有这种写法String name;# ❌ 也不存在a1;# ✅ Python 只有这种方式9.2 原因分析Python 没有变量声明这个概念。变量是通过赋值操作动态创建的——第一次给一个名字赋值时这个名字就诞生了。不需要事先声明类型。print(a)# NameError: name a is not defineda1# 变量 a 此时才出现print(a)# 输出19.3 这带来的一个常见坑x10deffoo():print(x)# UnboundLocalErrorx20foo()报错信息是UnboundLocalError: local variable x referenced before assignment原因Python 在编译函数体时发现函数内有x 20就把x标记为局部变量。但执行时print(x)在x 20之前执行此时局部变量 x 还没被赋值——于是就 UnboundLocalError 了。解决方法是加global x声明或者把print(x)放到x 20之后。一句话Python 里你在函数内对一个变量赋值Python 就认为它是局部变量就算外层有同名全局变量也不会认。思考 总结本文从源码视角拆解了 Python 变量赋值的底层机制核心知识点梳理如下变量不是盒子是标签。a 1意思是把标签 a 贴到整数对象 1 上不是把 1 放进盒子 a 里。b a是给同一个对象再贴一张标签不是拷贝。这个认知是理解 Python 内存模型的基础。小整数池和字符串驻留。-5 到 256 的整数和符合标识符规则的短字符串Python 全局只维护一份。这就是为什么a 1; b 1; a is b为 True但a 300; b 300; a is b为 False。日常比较值用别用is。可变 vs 不可变决定了赋值行为。b a后修改可变对象 a如 list.appendb 也会跟着变因为指向同一对象。修改不可变对象 a如字符串拼接a 的标签搬到了新对象上b 不变。函数传参遵循同样的规则。Python 传参是传对象引用。不是值传递也不是引用传递。形参拿到的是能找到同一个对象的地址能不能通过它修改原对象取决于对象是可变还是不可变。引用计数是内存管理的第一道防线。每个对象维护一个引用计数计数归零就回收。但循环引用会让计数法失效Python 用分代垃圾回收作为补充。Python 没有变量声明。变量通过赋值动态创建函数内只要对一个变量赋值Python 就认为它是局部变量要读外层值必须显式声明global或nonlocal。一个自查清单□变量是贴在对象上的标签—— 记住了吗 □ is 和在什么场景下结果会不同 □ 列表和字符串在赋值后的修改行为为什么不一样 □ 函数传参时传列表和传整数结果为什么不同 □ 小整数池的范围是多少 □ 引用计数遇到循环引用时怎么办结尾各位小伙伴本文的内容到这里就全部结束了源码骑士在这里再次感谢您的阅读源码骑士 — Python 全栈 系统架构关注跟博主一起从源码视角深耕底层原理见证每一次成长❤️点赞让优质内容被更多人看见让知识传递更有力量⭐收藏把核心知识点存好在需要时随时查、随时用评论分享你的经验或疑问评论区一起交流避坑一键四连不要忘记给博主一键四连哦今日源码拆解达成️寄语技术之路难免有困惑但同行的人会让前进更有方向结语希望这篇文章能让你对 Python 变量的理解上一个台阶——从我这样写能跑通到我知道它在内存里干了什么。后面我们会继续拆解列表的底层结构、字典的 hash 表实现不见不散。不要忘记给博主一键四连哦
01-Python变量赋值的底层真相-你写的a=1到底发生了什么
文章目录Python 变量赋值的底层真相你写的 a 1 到底发生了什么导入语1 ~ 变量不是盒子变量是标签1.1 现象1.2 原因分析1.3 底层原理1.4 验证用 id() 查看内存地址2 ~ a 1 和 b 1 之后a 和 b 指向同一个 1 吗2.1 现象2.2 原因分析2.3 验证小整数池的边界2.4 解决方案3 ~ 不可变对象赋值时究竟不变在哪里3.1 现象3.2 原因分析3.3 核心对比3.4 用 id() 验证4 ~ 函数传参Python 到底是值传递还是引用传递4.1 现象4.2 原因分析4.3 一个更直观的类比4.4 变量说明5 ~ 字符串的驻留机制——你以为是同一个Python 也觉得是5.1 现象5.2 原因分析5.3 字符串驻留有什么卵用6 ~ is vs ——你以为在比大小其实在比地址6.1 现象6.2 原因分析6.3 什么时候用 is7 ~ 引用计数——Python 怎么知道什么时候该删掉一个对象7.1 现象7.2 原因分析7.3 一个容易被忽略的细节8 ~ 循环引用当两个对象互相指引用计数就失效了8.1 现象8.2 原因分析8.3 实战中的循环引用9 ~ 变量声明在 Python 里是不存在的9.1 现象9.2 原因分析9.3 这带来的一个常见坑思考 总结结尾Python 变量赋值的底层真相你写的a 1到底发生了什么文章简介大部分Python教程第一课就教变量赋值但很少有人把a 1背后发生了什么讲清楚。本文从CPython源码视角拆解Python变量赋值的底层机制变量不是盒子、赋值不是拷贝、内存里到底发生了什么。文章采用现象→原因→原理三段式讲解覆盖引用计数、小整数池、字符串驻留、is vs 的内存级区别、函数传参是传引用还是传值这个经典面试题、以及可变对象与不可变对象在赋值时的行为差异。文中每一组结论都配了可复现的代码验证读完你会彻底明白为什么别人说Python里一切皆对象、变量只是标签。 个人主页源码骑士❄专栏传送门《Android开发基础》《python基础课程》⭐️热衷从源码视角拆解技术底层原理将复杂架构讲得通俗易懂 源码骑士的简介5年Android Framework系统开发经验曾主导多项系统级性能优化专项技术栈覆盖Android系统全链路Binder/Handler/AMS/WMS/启动流程及Java后端全家桶Spring MyBatis Redis Oracle累计产出原创技术文章100篇文章以源码拆解为特色被读者评价为看一篇胜过啃一周文档导入语如果你写过Python你一定写过这行代码a1如果有人问你这行代码在内存里发生了什么你能讲清楚吗我面试过不少写了两三年Python的人问到这个问题时答案惊人的统一“变量a指向了整数对象1”。然后——就没有然后了。再追问 “1 这个对象放在内存哪里a 怎么找到它的b 1之后 a 和 b 是不是指向同一个 1” 对面就开始支支吾吾。这篇文章的目的很单纯把 Python 变量赋值背后的内存机制用现象 → 原因 → 原理的方式一层层拆开。每一个结论都配了你可以自己跑通的验证代码看完之后你对 Python 变量的理解会和以前完全不一样。1 ~ 变量不是盒子变量是标签1.1 现象先用一段代码来挑战一下直觉a[1,2,3]ba b.append(4)print(a)# 输出[1, 2, 3, 4]你明明改的是b为什么a也跟着变了1.2 原因分析Java/C 程序员容易把变量想象成盒子——int a 1就是往 a 这个盒子里放进一个整数 1。但Python不是这样。在 Python 里变量根本不是盒子。变量只是贴在对象身上的一张标签。a [1, 2, 3] → 创建一个列表对象在上面贴了张标签叫 a b a → 同一张列表对象上又贴了张标签叫 b b.append(4) → 通过标签 b 找到这个对象往里面加了 4 → a 和 b 指向同一个对象当然 a 也变了1.3 底层原理用一张内存图来理解Python 堆内存 ┌─────────────────────┐ │ list 对象 │ │[1,2,3]← a │ ← a 和 b 都指向这里 │ ← b │ │ │ │ b.append(4)之后 │ │[1,2,3,4]│ ← 同一个对象内容变了 └─────────────────────┘1.4 验证用id()查看内存地址a[1,2,3]baprint(id(a))# 输出2773139414208你的数字可能不同print(id(b))# 输出2773139414208和上面一样print(aisb)# 输出Trueid(a)和id(b)完全一样——证明了 a 和 b 确实是指向同一个内存地址。一句话结论Python 的赋值语句a xxx翻译过来的意思是把标签 a 贴到对象 xxx 上而不是把 xxx 拷贝到 a 这个盒子里。这个认知是理解后面所有内容的基石。2 ~a 1和b 1之后a 和 b 指向同一个 1 吗2.1 现象a1b1print(aisb)# 输出True ← 嗯不是两个不同的 1换成一个大一点的数字呢a300b300print(aisb)# 输出False ← 这次又是不同的了同一个赋值逻辑结果却不一样。这是 Python 最让人困惑的行为之一。2.2 原因分析Python 在启动时预先创建了一小批常用整数对象-5 到 256。这个小整数池里的整数全局只有一份不会重复创建。所以a 1——Python 直接从小整数池里拿出现成的 1 对象贴上标签 a。b 1——Python 又去找 1发现池里已经有了直接把标签 b 也贴上去。结果a 和 b 指向同一个 1a is b为True。而 300 不在小整数池范围内每次 300都会新建一个整数对象所以a is b为False。2.3 验证小整数池的边界# 边界测试print(-5is-5)# True 池内print(-5is-61)# True -5在池内-61 算出 -5还是从池里拿print(256is256)# True 池内print(257is257)# False 池外注意这是 CPython 的行为如果在同一个表达式里用两个 257 呢a257;b257print(aisb)# False分别创建不是同一个对象# 但如果在同一行print(257is257)# True ← 为什么同一行的 257 is 257 又变 True 了因为 CPython 编译器看到同一行有相同的不可变常量会做优化——直接共享同一个对象。这不是小整数池的功劳而是编译期常量折叠。2.4 解决方案你想干什么用哪个判断值是否相等判断是不是同一个对象is几乎只用来和None比较日常写代码用就够了。is只在需要判断是不是同一个对象时用比如if x is None。不要拿is去比较数字和字符串的值你以为在比大小其实在比地址。3 ~ 不可变对象赋值时究竟不变在哪里3.1 现象ahelloba aa worldprint(a)# 输出hello worldprint(b)# 输出hello没变同样是b a列表的例子中 b 跟着变了字符串的例子中 b 却没变。为什么3.2 原因分析字符串是不可变对象。a a world不是在原地修改原来的字符串而是创建了一个全新的字符串对象然后把标签 a 从旧对象上撕下来贴到新对象上。整个过程分三步1. ahello→ 在内存中创建一个 str 对象hello贴上标签 a2. ba → 同一个hello对象上再贴个标签 b3. aa world→ ① 先创建一个全新的 str 对象hello world→ ② 把标签 a 从旧对象hello上撕下来 → ③ 把标签 a 贴到新对象hello world上 → ④ 标签 b 还贴在原来的hello上 → b 不变3.3 核心对比可变对象list/dict/set不可变对象int/str/tuple修改内容原地修改所有标签都能看到变化无法原地修改修改 创建新对象b a后修改 ab 跟着变同一个对象b 不变a 的标签被贴到了新对象上内存变化修改前后是同一个地址每次修改都是新地址3.4 用id()验证ahelloprint(id(a))# 地址 Aaa worldprint(id(a))# 地址 B和地址 A 不同baprint(id(b))# 地址 B和 a 一样每行id()的输出值都不同说明每次修改字符串都在创建新对象。一句话总结列表中 b 跟着变是因为 a 和 b 看着同一个箱子箱子里东西被动过了。字符串中 b 不跟着变是因为箱子本身被换掉了——a 搬到了新箱子b 还留在旧箱子门口。4 ~ 函数传参Python 到底是值传递还是引用传递4.1 现象面试高频题。先看代码# 传不可变对象defchange_int(x):x200a100change_int(a)print(a)# 输出100没变# 传可变对象defchange_list(lst):lst.append(4)b[1,2,3]change_list(b)print(b)# 输出[1, 2, 3, 4]变了同一个函数传参机制传 int 没变传 list 变了。这到底是传值还是传引用4.2 原因分析Python 的传参既不是纯粹的传值也不是纯粹的传引用。准确的说法是传对象引用Pass by Object Reference。具体解释change_int(a)的执行过程1. 函数调用时形参 xa → x 也贴到了整数对象100上2. 执行 x200→ x 从100上撕下来贴到新对象200上3. 函数结束 → a 的标签从来没被撕过还是指向100change_list(b)的执行过程1. 函数调用时形参 lstb → lst 也贴到了列表对象[1,2,3]上2. 执行 lst.append(4)→ 通过 lst 找到列表对象原地修改3. 函数结束 → b 还是指向同一个列表对象而对象内容已经变了4.3 一个更直观的类比把 Python 的变量想象成便签贴纸传参就是把便签纸的复印件递给函数——复印件上写着同一个地址。如果你通过复印件找到房子把房子里的东西改了修改可变对象——原版便签纸上的地址没变但房子里的东西确实变了。如果你在复印件上重新写一个新地址对不可变对象重新赋值——原版便签纸不受影响。4.4 变量说明说法对不对准确说法“Python是值传递”❌ 不完全对int 没变但 list 变了值传递解释不了list的行为“Python是引用传递”❌ 也不对如果你传入 int 后改变它实参不会跟着变“Python传对象引用”✅ 准确形参获得的是对象的引用不是值的拷贝也不是变量本身的引用记住传参时函数内部拿到的是能找到同一个对象的地址。能不能通过这个地址改动原对象取决于对象本身支不支持原地修改可变 vs 不可变。5 ~ 字符串的驻留机制——你以为是同一个Python 也觉得是5.1 现象ahellobhelloprint(aisb)# 输出True ← 和小整数池一模一样的味道# 但这种情况chello worlddhello worldprint(cisd)# 输出True ← 编译器优化# 而如果动态拼出来ehellofhelloprint(eisf)# 输出True ← 这是编译时常量折叠# 真正动态的情况importsys gsys.intern(hello)hsys.intern(hello)print(gish)# 输出True ← intern 强制驻留5.2 原因分析Python 对字符串做了和整数类似的优化叫字符串驻留String Interning。规则如下情况是否会驻留说明编译期确定、只含字母数字下划线✅ 自动驻留符合标识符规范的短字符串编译期常量折叠✅a b在编译时就算出ab了动态拼接、含特殊字符❌ 不自动驻留运行时才确定的字符串用sys.intern()手动驻留✅ 强制驻留你主动要求 Python 缓存这个字符串5.3 字符串驻留有什么卵用Python 代码里到处是字符串——函数名、属性名、字典的 key。如果每出现一个__init__就要新建一份内存浪费不可估量。驻留机制保证相同的短字符串全 Python 进程只有一份字典查找 key 的时候直接用地址比较is速度比逐个字符比较快得多。# 你看不到的地方Python 在做大量驻留classMyClass:def__init__(self):pass# __init__ 这个字符串被驻留了所有类的 __init__ 都共享同一份6 ~isvs——你以为在比大小其实在比地址6.1 现象a[1,2,3]b[1,2,3]print(ab)# 输出True ← 内容相同print(aisb)# 输出False ← 但不是同一个对象cNonedNoneprint(cisd)# 输出True ← None 全局唯一6.2 原因分析操作比较什么中文a ba.__eq__(b)“a 和 b 内容相同吗”a is bid(a) id(b)“a 和 b 是同一个对象吗”调用的是对象的__eq__方法比较的是内容。is比较的是两个变量在内存中的地址是否相同不能被重载。6.3 什么时候用isis几乎只用在两个场景# 场景1判断是否为 None最常用PEP 8 官方推荐ifxisNone:pass# 场景2判断是否为特定单例较少见ifobjissentinel:pass其他一切比大小“比内容”比数值的场景老老实实用。用is去比较两个列表是否内容相同是新手典型错误。7 ~ 引用计数——Python 怎么知道什么时候该删掉一个对象7.1 现象importsys a[1,2,3]print(sys.getrefcount(a))# 输出2a 一个引用getrefcount 的参数临时又多一个baprint(sys.getrefcount(a))# 输出3多了一个引用delbprint(sys.getrefcount(a))# 输出2b 的引用没了dela# list 对象引用计数回到 0被垃圾回收7.2 原因分析Python 每个对象都有一个计数器记录着当前有多少个变量指向它。计数器归零时Python 就知道这个对象没人用了可以回收它占用的内存。a[1,2,3]→ 引用计数1a 指向它 ba → 引用计数2a 和 b 都指向它 del b → 引用计数1b 的引用解除 del a → 引用计数0没人引用了→ 触发垃圾回收7.3 一个容易被忽略的细节sys.getrefcount(a)的结果永远比你预想的多 1——因为把 a 作为参数传递给 getrefcount 这个操作本身也创建了一个临时引用。a42print(sys.getrefcount(a))# 输出可能 11一大堆不完全是 2# 因为 42 是小整数Python 内部很多地方都在引用它8 ~ 循环引用当两个对象互相指引用计数就失效了8.1 现象classNode:def__init__(self,name):self.namename self.refNoneaNode(A)bNode(B)a.refb b.refa# 现在 a 和 b 互相引用deladelb# 这两个 Node 对象还能被回收吗8.2 原因分析引用计数遇到循环引用就彻底歇菜。a 被删了b 对 a 的引用还在所以 a 的引用计数不是 0。同理 b 也不是 0。结果两个对象谁都回收不了——内存泄漏。Python 不是傻子它还有一个**辅助垃圾回收器分代垃圾回收**专门处理这种情况。它隔一段时间扫描所有可能形成环的对象容器类发现循环引用就主动断开回收。8.3 实战中的循环引用# 列表自己引用自己lst[1,2,3]lst.append(lst)# lst[3] 就是 lst 自己dellst# 引用计数不是 0要等 GC 来救好在 Python 的 GC 默认是打开的日常写代码基本不用操心。但如果你在性能敏感的场景大量创建容器对象又解开引用这个知识能帮你理解为什么内存不会立刻释放。9 ~ 变量声明在 Python 里是不存在的9.1 现象Java 程序员转 Python 最常见的一个习惯inta;# ❌ Python 没有这种写法String name;# ❌ 也不存在a1;# ✅ Python 只有这种方式9.2 原因分析Python 没有变量声明这个概念。变量是通过赋值操作动态创建的——第一次给一个名字赋值时这个名字就诞生了。不需要事先声明类型。print(a)# NameError: name a is not defineda1# 变量 a 此时才出现print(a)# 输出19.3 这带来的一个常见坑x10deffoo():print(x)# UnboundLocalErrorx20foo()报错信息是UnboundLocalError: local variable x referenced before assignment原因Python 在编译函数体时发现函数内有x 20就把x标记为局部变量。但执行时print(x)在x 20之前执行此时局部变量 x 还没被赋值——于是就 UnboundLocalError 了。解决方法是加global x声明或者把print(x)放到x 20之后。一句话Python 里你在函数内对一个变量赋值Python 就认为它是局部变量就算外层有同名全局变量也不会认。思考 总结本文从源码视角拆解了 Python 变量赋值的底层机制核心知识点梳理如下变量不是盒子是标签。a 1意思是把标签 a 贴到整数对象 1 上不是把 1 放进盒子 a 里。b a是给同一个对象再贴一张标签不是拷贝。这个认知是理解 Python 内存模型的基础。小整数池和字符串驻留。-5 到 256 的整数和符合标识符规则的短字符串Python 全局只维护一份。这就是为什么a 1; b 1; a is b为 True但a 300; b 300; a is b为 False。日常比较值用别用is。可变 vs 不可变决定了赋值行为。b a后修改可变对象 a如 list.appendb 也会跟着变因为指向同一对象。修改不可变对象 a如字符串拼接a 的标签搬到了新对象上b 不变。函数传参遵循同样的规则。Python 传参是传对象引用。不是值传递也不是引用传递。形参拿到的是能找到同一个对象的地址能不能通过它修改原对象取决于对象是可变还是不可变。引用计数是内存管理的第一道防线。每个对象维护一个引用计数计数归零就回收。但循环引用会让计数法失效Python 用分代垃圾回收作为补充。Python 没有变量声明。变量通过赋值动态创建函数内只要对一个变量赋值Python 就认为它是局部变量要读外层值必须显式声明global或nonlocal。一个自查清单□变量是贴在对象上的标签—— 记住了吗 □ is 和在什么场景下结果会不同 □ 列表和字符串在赋值后的修改行为为什么不一样 □ 函数传参时传列表和传整数结果为什么不同 □ 小整数池的范围是多少 □ 引用计数遇到循环引用时怎么办结尾各位小伙伴本文的内容到这里就全部结束了源码骑士在这里再次感谢您的阅读源码骑士 — Python 全栈 系统架构关注跟博主一起从源码视角深耕底层原理见证每一次成长❤️点赞让优质内容被更多人看见让知识传递更有力量⭐收藏把核心知识点存好在需要时随时查、随时用评论分享你的经验或疑问评论区一起交流避坑一键四连不要忘记给博主一键四连哦今日源码拆解达成️寄语技术之路难免有困惑但同行的人会让前进更有方向结语希望这篇文章能让你对 Python 变量的理解上一个台阶——从我这样写能跑通到我知道它在内存里干了什么。后面我们会继续拆解列表的底层结构、字典的 hash 表实现不见不散。不要忘记给博主一键四连哦