当前位置:首页 > 文章列表 > 文章 > python教程 > Python可变与不可变对象详解

Python可变与不可变对象详解

2025-09-29 14:32:26 0浏览 收藏

积累知识,胜过积蓄金银!毕竟在文章开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《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_listid() 也会变,因为你让它指向了一个全新的列表对象。所以,关键在于区分是“原地修改”还是“重新赋值”。

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_paramstr_paramid 变了,但函数外部 my_nummy_strid 及其值都保持不变。这是因为 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_paramdict_paramid 在原地修改后并没有变,而且外部的 my_listmy_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. 查找失败: 如果列表内容改变,其哈希值也随之改变,那么当你尝试用 [1, 2, 3] 去查找字典中原本用 [1, 2] 存储的值时,计算出的哈希值已经不同了,字典将无法找到这个键,即使逻辑上它还是“同一个”对象。
  2. 数据完整性被破坏: 这种不稳定性会彻底破坏哈希表的查找机制,导致字典或集合无法正常工作,甚至可能出现内部数据结构混乱。

为了避免这种灾难性的后果,Python明确规定,只有哈希值在对象生命周期内保持不变的对象,才能作为字典的键或集合的元素。而只有不可变对象才能保证这一点,因为它们的内部状态一旦创建就不能改变,所以它们的哈希值也是固定的。

Python的强制执行 如果你尝试将一个可变对象(如列表或字典)作为字典的键或集合的元素,Python会直接抛出一个 TypeErrorunhashable 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 == obj2True),那么它们的哈希值必须相等(即 hash(obj1) == hash(obj2))。
  • 哈希值在对象的生命周期内必须保持不变。

不可变对象天生满足这些条件,因为它们的值不会变。而可变对象,由于其值可能变化,因此通常不实现 __hash__ 方法,或者其 __hash__ 方法会返回一个 TypeError

frozenset 的特殊性 值得一提的是 frozenset。它是 set 的不可变版本。由于它是不可变的,所以它可以被哈希,因此可以作为字典的键或集合的元素。这在某些需要以集合作为键的场景中非常有用。

在我看来,这个设计体现了Python在实用性和数据完整性之间的权衡。虽然限制了可变对象的使用场景,但它确保了字典和集合这些核心数据结构的高效性和可靠性,避免了潜在的复杂问题。理解这一点,能帮助我们更好地选择合适的数据结构来解决问题。

今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

HTML特殊字符编码怎么处理HTML特殊字符编码怎么处理
上一篇
HTML特殊字符编码怎么处理
海棠书屋热门小说入口及推荐地址
下一篇
海棠书屋热门小说入口及推荐地址
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    499次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • WisPaper:复旦大学智能科研助手,AI文献搜索、阅读与总结
    WisPaper
    WisPaper是复旦大学团队研发的智能科研助手,提供AI文献精准搜索、智能翻译与核心总结功能,助您高效搜读海量学术文献,全面提升科研效率。
    44次使用
  • Canva可画AI简历生成器:智能制作专业简历,高效求职利器
    Canva可画-AI简历生成器
    探索Canva可画AI简历生成器,融合AI智能分析、润色与多语言翻译,提供海量专业模板及个性化设计。助您高效创建独特简历,轻松应对各类求职挑战,提升成功率。
    47次使用
  • AI 试衣:潮际好麦,电商营销素材一键生成
    潮际好麦-AI试衣
    潮际好麦 AI 试衣平台,助力电商营销、设计领域,提供静态试衣图、动态试衣视频等全方位服务,高效打造高质量商品展示素材。
    151次使用
  • 蝉妈妈AI:国内首个电商垂直大模型,抖音增长智能助手
    蝉妈妈AI
    蝉妈妈AI是国内首个聚焦电商领域的垂直大模型应用,深度融合独家电商数据库与DeepSeek-R1大模型。作为电商人专属智能助手,它重构电商运营全链路,助力抖音等内容电商商家实现数据分析、策略生成、内容创作与效果优化,平均提升GMV 230%,是您降本增效、抢占增长先机的关键。
    314次使用
  • 社媒分析AI:数说Social Research,用AI读懂社媒,驱动增长
    数说Social Research-社媒分析AI Agent
    数说Social Research是数说故事旗下社媒智能研究平台,依托AI Social Power,提供全域社媒数据采集、垂直大模型分析及行业场景化应用,助力品牌实现“数据-洞察-决策”全链路支持。
    213次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码