python中可变对象和不可变对象是什么?

裘德小鎮的故事
发布: 2025-09-22 12:53:01
原创
356人浏览过
可变对象允许原地修改内容且内存地址不变,如列表、字典;不可变对象一旦创建内容不可变,任何“修改”都生成新对象,如整数、字符串、元组。该区别影响变量赋值、函数传参及数据结构使用,尤其在函数中对可变参数的原地修改会影响外部对象,而不可变对象则不会;此外,只有不可变对象才能作为字典键或集合元素,因其哈希值需稳定,确保哈希表机制正常工作。

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免费学习笔记(深入)”;

我觉得,这个区分之所以重要,是因为它直接影响到我们对变量赋值、函数参数传递以及数据结构行为的理解。有时候,我们可能会因为对这个概念的模糊,而写出一些有意外副作用的代码,尤其是在函数调用或者多线程环境中。

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精要参考 pdf版
Python精要参考 pdf版

这本书给出了一份关于python这门优美语言的精要的参考。作者通过一个完整而清晰的入门指引将你带入python的乐园,随后在语法、类型和对象、运算符与表达式、控制流函数与函数编程、类及面向对象编程、模块和包、输入输出、执行环境等多方面给出了详尽的讲解。如果你想加入 python的世界,David M beazley的这本书可不要错过哦。 (封面是最新英文版的,中文版貌似只译到第二版)

Python精要参考 pdf版 1
查看详情 Python精要参考 pdf版

可变对象和不可变对象在函数参数传递中有什么区别?

在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. 查找失败: 如果列表内容改变,其哈希值也随之改变,那么当你尝试用
    [1, 2, 3]
    登录后复制
    去查找字典中原本用
    [1, 2]
    登录后复制
    存储的值时,计算出的哈希值已经不同了,字典将无法找到这个键,即使逻辑上它还是“同一个”对象。
  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在实用性和数据完整性之间的权衡。虽然限制了可变对象的使用场景,但它确保了字典和集合这些核心数据结构的高效性和可靠性,避免了潜在的复杂问题。理解这一点,能帮助我们更好地选择合适的数据结构来解决问题。

以上就是python中可变对象和不可变对象是什么?的详细内容,更多请关注php中文网其它相关文章!

python速学教程(入门到精通)
python速学教程(入门到精通)

python怎么学习?python怎么入门?python在哪学?python怎么学才快?不用担心,这里为大家提供了python速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号