登录
首页 >  文章 >  python教程

Python可变与不可变对象详解

时间:2025-09-29 14:32:26 148浏览 收藏

积累知识,胜过积蓄金银!毕竟在文章开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《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学习网公众号,一起学习编程~

相关阅读
更多>
最新阅读
更多>
课程推荐
更多>