Python可变与不可变对象详解
积累知识,胜过积蓄金银!毕竟在文章开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《Python中可变与不可变对象的区别》,就带大家讲解一下知识点,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
可变对象允许原地修改内容且内存地址不变,如列表、字典;不可变对象一旦创建内容不可变,任何“修改”都生成新对象,如整数、字符串、元组。该区别影响变量赋值、函数传参及数据结构使用,尤其在函数中对可变参数的原地修改会影响外部对象,而不可变对象则不会;此外,只有不可变对象才能作为字典键或集合元素,因其哈希值需稳定,确保哈希表机制正常工作。
Python中的可变对象和不可变对象,简单来说,核心区别在于它们被创建之后,其内部状态能否被修改。可变对象允许你在不改变其内存地址的前提下修改其内容,而不可变对象一旦创建,其值就固定了,任何看起来是“修改”的操作,实际上都是创建了一个新的对象。理解这一点,对于我们在Python中处理数据、避免一些隐蔽的bug,以及优化代码性能,都至关重要。
解决方案
在Python的世界里,对象的这种“变”与“不变”属性,是其数据模型中一个非常基础但又极其关键的特性。它不仅仅是理论上的概念,更是实实在在影响我们日常编码行为的。
不可变对象 (Immutable Objects) 这类对象,一旦创建,其内部状态(值)就不能被改变。如果你尝试修改一个不可变对象,Python并不会在原地修改它,而是会创建一个新的对象,并将变量引用指向这个新对象。 常见的不可变对象包括:
- 数字 (Numbers):
int
,float
,complex
,bool
- 字符串 (Strings):
str
- 元组 (Tuples):
tuple
- 冻结集合 (Frozensets):
frozenset
举个例子,当我们写 a = 10
,然后 a = a + 1
时,表面上看是把 a
的值从10改成了11。但实际上,Python首先创建了一个值为10的整数对象,让 a
指向它;然后,在执行 a + 1
时,Python创建了一个值为11的新整数对象,最后让 a
重新指向这个新对象。原来的值为10的对象,如果不再被任何变量引用,就会被垃圾回收。
可变对象 (Mutable Objects) 与不可变对象相反,可变对象在创建后,其内部状态可以被修改,而且这种修改是“原地”进行的,也就是说,对象的内存地址通常不会改变。 常见的可变对象包括:
- 列表 (Lists):
list
- 字典 (Dictionaries):
dict
- 集合 (Sets):
set
- 字节数组 (Bytearrays):
bytearray
比如,my_list = [1, 2, 3]
。如果我们执行 my_list.append(4)
,这个列表对象本身并没有变,它还是那个列表,只是其内部多了一个元素。它的内存地址(可以用 id()
函数查看)通常是保持不变的。这种原地修改的能力,让可变对象在处理需要频繁增删改查的数据时显得非常灵活和高效。
我觉得,这个区分之所以重要,是因为它直接影响到我们对变量赋值、函数参数传递以及数据结构行为的理解。有时候,我们可能会因为对这个概念的模糊,而写出一些有意外副作用的代码,尤其是在函数调用或者多线程环境中。
Python中如何判断一个对象是可变还是不可变?
判断一个对象是可变还是不可变,其实有好几种方法,有些直观,有些则需要一点点代码验证。我个人最常用的,也是最直接的方法,就是利用 id()
函数来观察其内存地址的变化,或者干脆就记住那些常见的类型。
1. 使用 id()
函数观察内存地址:
这是最靠谱的验证方法。id()
函数会返回一个对象的唯一标识符,这个标识符在对象的生命周期内是不会改变的,通常可以理解为它的内存地址。
对于不可变对象: 任何“修改”操作后,对象的
id()
值都会发生变化,因为它实际上是创建了一个新对象。num = 10 print(f"原始数字的ID: {id(num)}") # 比如:140737352316480 num = num + 1 # 看起来是修改,实则创建新对象 print(f"修改后数字的ID: {id(num)}") # 比如:140737352316512 (ID变了) s = "hello" print(f"原始字符串的ID: {id(s)}") # 比如:2346048560304 s += " world" # 同样是创建新字符串 print(f"修改后字符串的ID: {id(s)}") # 比如:2346048560464 (ID变了)
对于可变对象: 在进行原地修改操作(如
append
,extend
,pop
,update
等)后,对象的id()
值会保持不变,因为它是在原有对象上进行修改。my_list = [1, 2, 3] print(f"原始列表的ID: {id(my_list)}") # 比如:2346048560640 my_list.append(4) # 原地修改 print(f"修改后列表的ID: {id(my_list)}") # 比如:2346048560640 (ID不变) my_dict = {'a': 1} print(f"原始字典的ID: {id(my_dict)}") # 比如:2346048560768 my_dict['b'] = 2 # 原地修改 print(f"修改后字典的ID: {id(my_dict)}") # 比如:2346048560768 (ID不变)
这里有个小陷阱,如果你对可变对象进行赋值操作,比如
my_list = [5, 6]
,那么my_list
的id()
也会变,因为你让它指向了一个全新的列表对象。所以,关键在于区分是“原地修改”还是“重新赋值”。
2. 查阅文档或记住常见类型: 这是最省事的方法。Python的官方文档对每种内置类型都有详细说明。一般来说:
- 不可变:
int
,float
,str
,tuple
,frozenset
- 可变:
list
,dict
,set
,bytearray
3. 尝试调用修改方法:
如果一个对象有 append()
, extend()
, insert()
, pop()
, remove()
, sort()
(针对列表) 或 add()
, update()
, clear()
(针对集合/字典) 等方法,并且这些方法会改变对象自身的内容,那么它很可能就是可变对象。不可变对象通常没有这些原地修改的方法,或者其方法(如字符串的 replace()
)会返回一个新的对象。
我个人觉得,对于初学者来说,先记住那些常见的可变和不可变类型,然后通过 id()
函数去验证和加深理解,是最好的学习路径。这能帮助你建立起对Python内存管理更直观的感受。
可变对象和不可变对象在函数参数传递中有什么区别?
在Python中,函数参数传递采用的是“传对象引用”(pass-by-object-reference)的机制,这和C++的“传值”或“传引用”有所不同,它介于两者之间,但又独具特色。可变对象和不可变对象在这个机制下的行为差异,是很多Python新手容易混淆,甚至老手也偶尔会踩坑的地方。
1. 传递不可变对象作为参数: 当你将一个不可变对象(如整数、字符串、元组)作为参数传递给函数时,函数内部对这个参数的任何“修改”操作,实际上都会在函数内部创建一个新的局部变量,并让这个局部变量指向新的对象。原始的外部对象不会受到任何影响。
def modify_immutable(num_param, str_param): print(f"函数内部 - 原始数字ID: {id(num_param)}") num_param += 1 # 实际是创建了一个新的整数对象,num_param指向它 print(f"函数内部 - 修改后数字ID: {id(num_param)}") print(f"函数内部 - 修改后数字: {num_param}") print(f"函数内部 - 原始字符串ID: {id(str_param)}") str_param += " world" # 实际是创建了一个新的字符串对象 print(f"函数内部 - 修改后字符串ID: {id(str_param)}") print(f"函数内部 - 修改后字符串: {str_param}") my_num = 10 my_str = "hello" print(f"函数外部 - 原始数字ID: {id(my_num)}") print(f"函数外部 - 原始字符串ID: {id(my_str)}") modify_immutable(my_num, my_str) print(f"函数外部 - 调用后数字: {my_num}, ID: {id(my_num)}") # 外部my_num不变 print(f"函数外部 - 调用后字符串: {my_str}, ID: {id(my_str)}") # 外部my_str不变
你会发现,函数内部 num_param
和 str_param
的 id
变了,但函数外部 my_num
和 my_str
的 id
及其值都保持不变。这是因为 num_param += 1
这样的操作,对于不可变对象来说,意味着重新绑定到一个新对象。
2. 传递可变对象作为参数:
当你将一个可变对象(如列表、字典、集合)作为参数传递给函数时,函数内部和外部的变量都指向同一个对象。因此,如果在函数内部对这个可变对象进行“原地修改”操作(例如 list.append()
, dict.update()
),这些修改会直接反映到函数外部的原始对象上。
def modify_mutable(list_param, dict_param): print(f"函数内部 - 原始列表ID: {id(list_param)}") list_param.append(4) # 原地修改,外部列表也会受影响 print(f"函数内部 - 修改后列表ID: {id(list_param)}") print(f"函数内部 - 修改后列表: {list_param}") print(f"函数内部 - 原始字典ID: {id(dict_param)}") dict_param['c'] = 3 # 原地修改,外部字典也会受影响 print(f"函数内部 - 修改后字典ID: {id(dict_param)}") print(f"函数内部 - 修改后字典: {dict_param}") my_list = [1, 2, 3] my_dict = {'a': 1, 'b': 2} print(f"函数外部 - 原始列表ID: {id(my_list)}") print(f"函数外部 - 原始字典ID: {id(my_dict)}") modify_mutable(my_list, my_dict) print(f"函数外部 - 调用后列表: {my_list}, ID: {id(my_list)}") # 外部my_list已改变 print(f"函数外部 - 调用后字典: {my_dict}, ID: {id(my_dict)}") # 外部my_dict已改变
这里,函数内部的 list_param
和 dict_param
的 id
在原地修改后并没有变,而且外部的 my_list
和 my_dict
也确实被修改了。这是因为函数内部和外部的变量都指向同一个对象,对这个对象的任何原地修改,都会被所有引用它的变量“看到”。
一个重要的例外:在函数内部重新赋值可变参数
如果我们在函数内部,对传入的可变参数进行重新赋值操作(例如 list_param = [5, 6]
),那么这个行为又会和不可变对象类似。函数内部的 list_param
会被重新绑定到一个新的列表对象,而外部的 my_list
仍然指向原来的列表,不会受到影响。
def reassign_mutable(list_param): print(f"函数内部 - 原始列表ID: {id(list_param)}") list_param = [5, 6] # 重新赋值,list_param现在指向一个新对象 print(f"函数内部 - 重新赋值后列表ID: {id(list_param)}") print(f"函数内部 - 重新赋值后列表: {list_param}") my_list_reassign = [1, 2, 3] print(f"函数外部 - 原始列表ID: {id(my_list_reassign)}") reassign_mutable(my_list_reassign) print(f"函数外部 - 调用后列表: {my_list_reassign}, ID: {id(my_list_reassign)}") # 外部my_list_reassign不变
这块儿确实容易踩坑,因为它模糊了“原地修改”和“重新赋值”的区别。我个人觉得,理解这个点对于编写健壮的Python代码非常关键,尤其是在设计函数接口时,要清楚函数是否会修改传入的可变参数,避免产生意料之外的副作用。如果不想函数修改原始的可变对象,可以考虑在传入前先创建一份副本(例如 my_list[:]
或 list(my_list)
)。
为什么只有不可变对象才能作为字典的键或集合的元素?
这是一个非常好的问题,它触及到了Python中字典(dict
)和集合(set
)底层实现的关键机制:哈希表(hash table)。简而言之,只有不可变对象才能作为字典的键或集合的元素,是因为它们需要一个在对象的生命周期内保持不变的哈希值(hash value)。
哈希值与哈希表 字典和集合为了实现高效的查找、插入和删除操作,都依赖于哈希表这种数据结构。当我们把一个对象作为字典的键或集合的元素时,Python会计算这个对象的哈希值。这个哈希值可以看作是对象在内存中存储位置的一个“指纹”或“索引”。
- 字典: 当你尝试查找一个键对应的值时,Python会再次计算这个键的哈希值,然后根据这个哈希值快速定位到可能的存储位置,再通过
__eq__
方法比较键是否完全匹配。 - 集合: 集合的工作原理类似,它使用哈希值来判断元素是否存在,并确保元素的唯一性。
哈希值的稳定性要求
想象一下,如果一个可变对象(比如一个列表 [1, 2]
)可以作为字典的键。当你把它放进字典后,它的哈希值是根据 [1, 2]
计算出来的。但如果之后你修改了这个列表,比如 my_list.append(3)
,那么这个列表就变成了 [1, 2, 3]
。此时,如果Python允许它作为键,那么它的哈希值也应该随之改变。
问题就在这里:
- 查找失败: 如果列表内容改变,其哈希值也随之改变,那么当你尝试用
[1, 2, 3]
去查找字典中原本用[1, 2]
存储的值时,计算出的哈希值已经不同了,字典将无法找到这个键,即使逻辑上它还是“同一个”对象。 - 数据完整性被破坏: 这种不稳定性会彻底破坏哈希表的查找机制,导致字典或集合无法正常工作,甚至可能出现内部数据结构混乱。
为了避免这种灾难性的后果,Python明确规定,只有哈希值在对象生命周期内保持不变的对象,才能作为字典的键或集合的元素。而只有不可变对象才能保证这一点,因为它们的内部状态一旦创建就不能改变,所以它们的哈希值也是固定的。
Python的强制执行
如果你尝试将一个可变对象(如列表或字典)作为字典的键或集合的元素,Python会直接抛出一个 TypeError
:unhashable type: 'list'
或 unhashable type: 'dict'
。
# 错误示例 my_list_key = [1, 2] # my_dict = {my_list_key: "value"} # 这会引发 TypeError # 错误示例 my_set = set() # my_set.add([1, 2]) # 这也会引发 TypeError
__hash__
和 __eq__
方法
在Python中,一个对象是否可哈希(hashable),取决于它是否实现了 __hash__
方法,并且满足以下条件:
- 如果两个对象相等(即
obj1 == obj2
为True
),那么它们的哈希值必须相等(即hash(obj1) == hash(obj2)
)。 - 哈希值在对象的生命周期内必须保持不变。
不可变对象天生满足这些条件,因为它们的值不会变。而可变对象,由于其值可能变化,因此通常不实现 __hash__
方法,或者其 __hash__
方法会返回一个 TypeError
。
frozenset
的特殊性
值得一提的是 frozenset
。它是 set
的不可变版本。由于它是不可变的,所以它可以被哈希,因此可以作为字典的键或集合的元素。这在某些需要以集合作为键的场景中非常有用。
在我看来,这个设计体现了Python在实用性和数据完整性之间的权衡。虽然限制了可变对象的使用场景,但它确保了字典和集合这些核心数据结构的高效性和可靠性,避免了潜在的复杂问题。理解这一点,能帮助我们更好地选择合适的数据结构来解决问题。
今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

- 上一篇
- HTML特殊字符编码怎么处理

- 下一篇
- 海棠书屋热门小说入口及推荐地址
-
- 文章 · python教程 | 13分钟前 |
- Python获取当前脚本路径的几种方法
- 427浏览 收藏
-
- 文章 · python教程 | 50分钟前 |
- SQLAlchemy元数据序列化优化技巧
- 375浏览 收藏
-
- 文章 · python教程 | 52分钟前 | 数据分析 可视化 Pandas EDA JupyterNotebook
- JupyterNotebook数据分析实用指南
- 372浏览 收藏
-
- 文章 · python教程 | 1小时前 | Python 异常 自定义异常 try...except raise
- Python中raise抛出异常的用法
- 298浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- 单下划线与双下划线:_var、__var、__var__区别详解
- 256浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- Pythonf-string格式化全解析
- 500浏览 收藏
-
- 文章 · python教程 | 1小时前 | io.StringIO Python装饰器 sys.stdout 屏蔽输出 重定向输出
- Python装饰器屏蔽日志教程
- 470浏览 收藏
-
- 文章 · python教程 | 2小时前 | Python PlotlyExpress 交互式地图 px.choropleth px.scatter_mapbox
- Python交互式地图制作:PlotlyExpress教程
- 434浏览 收藏
-
- 文章 · python教程 | 2小时前 |
- Python代码运行时间测量技巧
- 219浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python虚拟环境创建全攻略
- 204浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python字典嵌套提取方法详解
- 284浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- WisPaper
- WisPaper是复旦大学团队研发的智能科研助手,提供AI文献精准搜索、智能翻译与核心总结功能,助您高效搜读海量学术文献,全面提升科研效率。
- 44次使用
-
- Canva可画-AI简历生成器
- 探索Canva可画AI简历生成器,融合AI智能分析、润色与多语言翻译,提供海量专业模板及个性化设计。助您高效创建独特简历,轻松应对各类求职挑战,提升成功率。
- 47次使用
-
- 潮际好麦-AI试衣
- 潮际好麦 AI 试衣平台,助力电商营销、设计领域,提供静态试衣图、动态试衣视频等全方位服务,高效打造高质量商品展示素材。
- 151次使用
-
- 蝉妈妈AI
- 蝉妈妈AI是国内首个聚焦电商领域的垂直大模型应用,深度融合独家电商数据库与DeepSeek-R1大模型。作为电商人专属智能助手,它重构电商运营全链路,助力抖音等内容电商商家实现数据分析、策略生成、内容创作与效果优化,平均提升GMV 230%,是您降本增效、抢占增长先机的关键。
- 314次使用
-
- 数说Social Research-社媒分析AI Agent
- 数说Social Research是数说故事旗下社媒智能研究平台,依托AI Social Power,提供全域社媒数据采集、垂直大模型分析及行业场景化应用,助力品牌实现“数据-洞察-决策”全链路支持。
- 213次使用
-
- Flask框架安装技巧:让你的开发更高效
- 2024-01-03 501浏览
-
- Django框架中的并发处理技巧
- 2024-01-22 501浏览
-
- 提升Python包下载速度的方法——正确配置pip的国内源
- 2024-01-17 501浏览
-
- Python与C++:哪个编程语言更适合初学者?
- 2024-03-25 501浏览
-
- 品牌建设技巧
- 2024-04-06 501浏览