Python变量赋值详解:内存模型、解包与交换变量

发布时间:2026/10/13 17:42:30
Python变量赋值详解:内存模型、解包与交换变量 写这篇博文之前我想先坦白一件事我见过不少写Python写了三五年的开发者能在sorted、functools这些进阶话题上侃侃而谈结果被一道a b []和a []; b []有什么区别的题目问住。变量赋值在Python里太基础了基础到很多人跳过了它直接去背语法、学框架。但这恰恰是很多隐蔽bug的源头。这篇是Python变量详解系列的第三篇就专门把赋值这件事彻底掰开揉碎从内存模型讲到解包技巧再聊到交换变量值的底层原理一次性讲透。适合谁看刚学Python、对id()和类型还发怵的新手看这篇能把地基打牢写了一段时间但老被列表改了一个另一个也变了坑的开发者这篇里有完整的解释和排查思路哪怕是带团队做Code Review的看完也能多几个能跟人掰扯的硬知识点。1. 变量赋值的基础认知1.1 赋值语句的本质是绑定不是复制很多人学变量时脑子里残留的是C语言那套模型变量是一个盒子赋值就是把值装进盒子里。这个模型在Python里是错的而且错得很危险。Python里真正的模型是名字-对象绑定。当你写a [1, 2, 3]实际发生的事情是先在内存里创建了一个列表对象[1, 2, 3]然后让名字a指向引用这个对象。a本身不存数据存的是一个指向对象的引用。用一个类比来理解变量名是贴在储物柜上的标签对象是储物柜本身。a [1, 2, 3]的意思是给这个装着列表的储物柜贴一张叫a的标签。你再写b a不是把储物柜复制了一份而是给同一个储物柜再贴一张写着b的标签。所以b.append(4)之后你去看a里面也有4——因为两个标签指着同一个柜子。理解了这一点Python赋值相关的大部分坑就已经规避了一半。判断两个变量是不是指向同一个对象标准动作是用is或者比较id()a [1, 2] b a print(a is b) # True print(id(a), id(b)) # 完全相同的地址这里我想强调一个高频误判点很多人喜欢用判断变量是否相同。但比较的是值is比较的是身份。值相同不代表身份相同后面讲数据类型时还会碰到这个细节。1.2 赋值与引用计数为什么del不掉所有引用既然变量名只是标签那删除变量这个概念就值得重新理解了。a [1, 2, 3] b a del a执行del a之后被删除的是标签a不是列表对象。列表对象还活着因为b仍然引用着它。什么时候对象才真正被回收等所有引用都没了引用计数降到0Python的垃圾回收机制才会把它清理掉。这个设计带来的直接后果就是你在函数里传参时传的是引用。看这个经典例子def add_item(lst): lst.append(100) my_list [1, 2] add_item(my_list) print(my_list) # [1, 2, 100]函数内部明明没有return外面的my_list却被改了。原因就是参数传递传的是引用函数里的lst和外面的my_list指向同一个对象。这个特性在写代码时是把双刃剑想避免副作用可以用切片或list()拷贝一份再操作想原地修改就直接传引用。提示如果你在函数里对参数执行lst lst [100]效果完全不同——会创建新列表并绑定给lst外面那个变量完全不受影响。新手经常在这个lst.append()和lst lst []之间来回困惑区分原地修改和重新绑定是理解赋值的关键。2. 数据类型与动态类型2.1 常用数据类型速览与判断方法Python常见的内置数据类型并不算多但类型之间的行为差异会影响赋值语义值得先梳理一遍类型示例是否可变int1, 100, -5否float3.14, 2.0否boolTrue, False否strhello否bytesbdata否list[1, 2]是tuple(1, 2)否dict{a: 1}是set{1, 2}是为什么要先记住可变性因为不可变对象意味着一旦创建你无法原地修改它的内容。变量重新赋值时实际上是让名字指向了一个全新对象。最典型的就是整数a 100 print(id(a)) a a 1 print(id(a)) # 地址变了a 1创建了一个新的整数对象然后a被重新绑定到新对象原来的100被丢弃。正因为不可变对象永远不会原地变化所以多个变量指向同一个不可变对象时你完全不用担心改了其中一个其他也变——你想改都改不了。判断类型的常规姿势有两个type(x)和isinstance(x, T)。我强烈建议在业务代码里用isinstance而不是type因为type判断的是精确类型会拒绝子类实例isinstance允许子类通过更符合面向对象设计的预期。class MyInt(int): pass x MyInt(5) print(type(x) is int) # False因为是MyInt print(isinstance(x, int)) # TrueMyInt是int的子类2.2 动态类型的底层逻辑Python是动态类型语言这句话很多教程都说过但真正理解的人不多。动态类型有两个层面的含义其一变量本身没有类型约束。同一个变量名今天可以绑定一个整数明天绑定一个字符串后天绑一个函数解释器都不会报错x 42 x hello x lambda: hi每次赋值都只是换了一张标签对象自身是什么类型跟变量名没有固定关系。其二类型是对象的属性不是变量名的属性。你在交互式环境里输入type(x)它实际查询的是x当前绑定的那个对象是什么类型。这就带来一个重要推论写Python代码时变量的类型是运行时才确定的IDE和静态检查工具只能做推断和提醒不能保证正确性。这个特性给开发带来了极大的灵活度也埋下了隐患。同一个函数传入整数和字符串行为完全不同def double(x): return x x print(double(5)) # 10 print(double(ab)) # abab在整数上做加法在字符串上做拼接。这是动态类型的魔法也是鸭子类型的体现只要对象支持操作函数就来者不拒。想约束行为可以配合类型注解def double(x: int) - int:加上静态检查工具但运行时它仍然不会强制校验。我先说明白这点免得你把类型注解当成运行时保护。2.3 可变对象赋值的别名陷阱可变对象是赋值陷阱的高发区。核心规律只有一条对可变对象执行赋值永远是给同一个对象贴新标签不会产生副本。a [1, 2, 3] b a # 别名不是拷贝 b.append(4) print(a) # [1, 2, 3, 4]如果你确实需要一个独立副本必须显式拷贝。列表有切片a[:]、list(a)、a.copy()三种常用方式字典用dict(d)或d.copy()集合用set(s)。嵌套结构还得用copy.deepcopy因为浅拷贝只复制外层容器里面的子列表仍然是共享的import copy original [[1, 2], [3, 4]] shallow original.copy() shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]]内层被改了 deep copy.deepcopy(original) deep[0].append(100) print(original) # 不受影响这个例子建议你亲手跑一遍。很多线上事故的根源就是开发者在某个配置项或缓存对象上做了浅拷贝然后修改了内层结构导致原始数据被污染。理解了别名和拷贝的区别这类问题一次就能根治。3. 多变量赋值与序列解包3.1 多变量赋值的几种正确写法Python允许一行代码给多个变量赋值这是语法糖但用好了非常顺手。最常见的形式是等号两边都是逗号分隔的序列a, b, c 10, 20, 30执行过程并不复杂Python会先把右边的(10, 20, 30)打包成一个元组再按位置一一解包赋值给左边的三个变量。所以右边写不写括号效果一样a, b, c (10, 20, 30) # 等价多变量赋值还能用于函数返回值的接收。函数返回元组时直接解包比用索引取值清爽得多def min_max(nums): return min(nums), max(nums) lo, hi min_max([3, 1, 4, 1, 5]) print(lo, hi) # 1 5我见过不少老代码这么写result divmod(7, 3) q result[0] r result[1]其实一行就能搞定q, r divmod(7, 3)这种写法不只是少了几行代码更重要的是它把一次性取出多个相关值这个意图直接表达出来了可读性显著提升。3.2 解包赋值支持哪些数据源很多初学者以为解包只能用在元组上其实任何可迭代对象都能被解包。列表、字符串、字典、集合、生成器通通可以x, y, z [1, 2, 3] # 列表 first, *rest hello # 字符串firsth k1, k2 {a: 1, b: 2} # 字典取的是键字符串解包时按字符拆字典解包时取键而不取值。集合也能解包但集合是无序的解包结果依赖迭代顺序不建议在业务逻辑里依赖这种顺序。解包的硬性约束是数量必须对得上否则直接抛ValueErrora, b 1, 2, 3 # ValueError: too many values to unpack a, b, c 1, 2 # ValueError: not enough values to unpack这个约束严谨得近乎苛刻也正是因此解包代码通常很安全——数量不匹配在运行第一时间就会报错而不会悄悄吞掉数据。3.3 带星号的解包与下划线占位解包最强大的进阶玩法是星号表达式*。它能一次性捕获剩余的所有元素从Python 3.0开始支持是日常工作中极其高频的神器first, *middle, last [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5注意middle拿到的是一个列表即使只有一个元素也是列表。这一点在写算法题时经常用到比如去掉数组首尾取中间部分。星号还有一个隐藏约束一个赋值语句里最多只能出现一个带星号的变量。first, *middle, *last ...直接是语法错误。原因是Python无法确定*middle和*last怎么切分——中间边界只有一个不能有两个。用下划线_作为占位符是Python社区约定俗成的习惯表示这个位置的值我不关心_, age, _ (某开发者, 25, 北京) print(age) # 25这里两个_其实是同一个变量第二次赋值会覆盖第一次。如果只做占位用没问题但如果后面还要用第一个_的值就会踩坑。更稳妥的做法是用_配合星号比如first, *_ items只取首元素、*_, last items只取尾元素剩下的一律忽略。嵌套解包也有不少应用场景。处理比较深的嵌套结构时可以按结构层层解开data (item, (10, 20), {status: ok}) name, (x, y), info data print(name, x, y, info[status])只要右边的结构和左边的模式一一对应Python就能精确解包。这个特性在处理接口返回的复杂JSON结构时特别好用比手写一连串data[xxx][yyy]取值要安全得多。注意解包时数量不匹配是运行时才报错的错误不是语法错误。意思是代码能编译通过跑起来才炸。所以涉及外部传入的可迭代数据时务必先确认长度或先用isinstance和len()做保护性判断不要让解包直接暴露在不可信的数据源上。4. 交换变量值的几种正确姿势4.1 Python原生的元组解包交换法交换两个变量的值几乎所有语言的教学里都会讲临时变量法temp a a b b tempPython提供了更优雅的等价写法一行搞定a, b b, a原理就是前面讲过的解包右边(b, a)先被构造成一个临时元组然后按顺序赋给左边。右侧的值在赋值开始前就已经完整保存在临时元组里了所以不存在先改了a导致b拿到错误值的问题。我想拆一下这个操作的底层过程因为很多人以为它是Python解释器做了什么魔法。实际执行时Python会引入一个栈顶旋转指令。你可以在反汇编里看到端倪import dis dis.dis(a, b b, a)输出里会出现SWAP相关指令不同Python版本指令名略有不同3.11以后是显式的SWAP指令它专门优化了这种交换操作。换句话说Python语言层面就为交换提供了原生支持这也是为什么Python代码里极少看到临时变量交换法——不是大家忘了而是有更好的工具。4.2 不同场景下的交换变体交换不仅限于两个独立变量。在列表里交换两个位置的元素可以直接用索引解包lst [1, 2, 3, 4] lst[0], lst[3] lst[3], lst[0] print(lst) # [4, 2, 3, 1]这个写法在洗牌、排序算法里非常常见。比如实现冒泡排序时需要比较相邻元素并互换位置def bubble_sort(arr): for i in range(len(arr) - 1): for j in range(len(arr) - 1 - i): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] return arr整段代码里交换只占一行且意图直白如果前一个比后一个大就换位置。没有临时变量没有多余的解释。三个变量循环交换也可以这么玩a, b, c c, a, b右边先构造包含三个原始值的临时元组再按新顺序绑定。操作和两个变量的情况完全一致只是构造的临时元组长一点。4.3 交换操作中的常见误区交换本身语法简单但我在实际Code Review里见过不少衍生问题值得提醒。第一个误区和上文的别名问题密切相关。如果你用解包交换两个引用同一对象的变量结果可能不是你想要的a [1, 2] b a a, b b, a经过这个操作a和b都还是指向同一个列表对象交换前后没有任何实际变化。因为右边拿到的本来就是同一个对象的引用赋值回去也还是同一个引用。交换操作交换的是标签指向不能让一个对象变成另一个对象。第二个误区出现在函数内部想交换外部变量时。如果你这么写def swap(x, y): x, y y, x a 1 b 2 swap(a, b) print(a, b) # 仍然是 (1, 2)函数内部交换的只是参数局部变量x、y的绑定对外面的a、b没有任何影响。参数的赋值是重新绑定局部名字不会穿过函数边界修改外部变量。想让函数影响外部变量要么return x, y后接收返回值要么直接传入可变容器修改其内容。第三个误区是性能强迫症。有段时间社区里流传Python的a, b b, a有额外元组开销不如临时变量快的说法。实测下来两种情况性能差距在纳秒级别对绝大多数程序毫无影响。Python解释器对解包交换有专门优化而且代码的可读性收益远大于那点微小的性能差异。除非你在写每秒执行千万次交换的热循环否则不需要为这个问题纠结。5. 常见问题与排查技巧实录5.1 最容易踩的五个赋值陷阱第一个陷阱a b []会创建共享列表。看下面的例子a b [] a.append(1) print(a) # [1] print(b) # [1]b也被改了链式赋值时a和b绑定到同一个新建的列表对象上。如果你想要两个独立的空列表必须分开创建a [] b []但a b 0是安全的因为整数不可变共享同一个整数对象没有任何危害。判断是否安全就看这个类型的对象能不能原地修改可变类型共享会出事不可变类型共享无所谓。第二个陷阱默认参数和可变对象。这是Python面试题里的常客def add_item(item, target[]): target.append(item) return target多次调用后target会累积所有调用传入的元素。因为默认参数在函数定义时只创建一次后续调用如果没传target用的都是同一个列表对象。正确的写法是用None作为默认值函数内部再创建新列表def add_item(item, targetNone): if target is None: target [] target.append(item) return target第三个陷阱判断变量类型时用了type()恰好遇到子类。前面说了用isinstance更健壮这里不再展开。第四个陷阱解包不等长导致程序崩溃。我遇到过真实案例某个接口返回的元组在正常数据下是三个字段某天上游数据格式微调后变成两个字段整条链路瞬间报ValueError。排查时的教训是对外部数据先做长度校验或者用*吸收多余的部分a, b, *_ external_data()这样结构变化时至少不会直接抛异常但也要谨慎使用——吸收多余的字段也可能掩盖上游数据的问题最好配合日志记录。第五个陷阱在循环里修改正在迭代的列表同时用别名操作。比如for item in my_list: if item 5: my_list.remove(item)这里item和my_list的元素是同一个引用remove会改变列表结构但迭代器仍然按原索引推进很容易跳过一个元素。赋值和可变对象的相互作用在循环里会放大建议遍历时对副本操作或改用列表推导。5.2 赋值问题排查速查表整理一份我平时排查问题时用的速查表按现象定位原因现象可能原因解决方案改了b结果a也变两者是同一个可变对象的别名用a[:]、list(a)或copy.deepcopy显式拷贝函数里改了参数外面也变传参是引用传递函数入口先拷贝或明确约定原地修改a, b 1, 2, 3报错解包数量不匹配检查数据长度用*吸收多余项默认参数累积数据默认参数是共享可变对象改用None哨兵值type(x) is int判断失败x是int的子类改用isinstance(x, int)交换了两个变量但值没变两者原本就是同一对象的引用检查是否在操作前就发生了别名绑定a, b, *c报语法错误同时用了两个星号解包星号表达式只能出现一次这张表不算详尽但覆盖了我工作中遇到的大部分赋值相关问诊。排查询问的通用思路是先确认变量名绑定的是不是同一个对象用id()和is再确认对象类型可变性最后看代码里有没有隐式的共享引用。这三步能解决九成以上的赋值诡异问题。5.3 三个值得长期坚持的编码习惯习惯一凡是两个变量之间存在一个是另一个复制品的意图立刻显式拷贝。不要靠现在还没人改它的侥幸撑过去。哪怕当时只有一个引用三个月后别人往函数里传这个变量时别名问题会以更隐蔽的方式爆发。习惯二解包赋值时能用*吸收多余项就用*。特别是处理外部数据源时这相当于给代码加了一层防御。但这层防御不能滥用要在吸收前打印或记录一下长度异常让问题在测试阶段暴露。习惯三用a, b b, a交换变量时先确认两者不是同一个对象的别名。尤其是从列表里取元素再做交换时先思考这个列表是独享的还是被多个变量引用的。如果是共享的改完记得通知所有相关方。我在实际开发里的体会是赋值这个概念本身不值钱值钱的是它背后那套引用-对象模型。把这个模型装进脑子里再回头看Python里各种奇奇怪怪的行为——闭包延迟绑定、默认参数陷阱、浅拷贝连锁修改——都不过是同一棵树上的不同枝杈。最后再分享一个小技巧调试赋值相关的问题时养成随手打印id()的习惯。比如print(id(a), id(b))两个地址相同就说明是同一个对象问题就转化为这个对象被谁改了地址不同就说明赋值过程已经创建了新对象问题就转化为为什么绑定到了新对象。判断快、定位准比肉眼对比列表内容直观得多。这一招帮我排查过不少莫名其妙被修改的线上问题也希望能在你的开发里派上用场。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询