python中的可变对象与不可变对象

本文深入解析Python中的可变与不可变数据类型,如列表、字典、整型、字符串等的特点,以及它们如何影响变量赋值和函数参数传递。通过实例演示,帮助读者理解Python内存管理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

python与C/C++不一样,它的变量使用有自己的特点,当初学python的时候,一定要记住“一切皆为对象,一切皆为对象的引用”这句话,其实这个特点类似于JAVA,所以在python里面大家也不用担心类似于C/C++中的指针的复杂问题。下面本文将对python里面的“可变数据类型”和“不可变数据类型”进行分析。

首先,我们需要知道在python中哪些是可变数据类型,哪些是不可变数据类型。可变数据类型:列表list和字典dict,set,自己定义的类对象,numpy中的ndarray对象,具体参考:NumPy:拷贝和视图。不可变数据类型:整型int、浮点型float、字符串型string和元组tuple,以及frozenset。(注意:字典的key只能是不可变对象,即字典的key只能是整型int、浮点型float、字符串型string和元组tuple)

set(): 一种可变的、无序的、有限的集合,其元素是唯一的、不可变的(可哈希的)对象。
frozenset(): 一种不可变的、可哈希的、无序的集合,其元素是唯一的,不可变的哈希对象。

其次需要注意,python中的变量保存的都是值所在的地址,例如 a = 10,a保存的是10所在的地址。这里的可变与不可变指的是指向可变或者不可变。

然后,我们以int和list为例,来看看“可变数据类型”和“不可变数据类型”到底有什么区别。

(1) 不可变数据类型分析 。先来看一段程序:

>>> x = 1
>>> id(x)
31106520
>>> y = 1
>>> id(y)
31106520
>>> x == y
True
>>> x is y #具有相同值的不可变对象(例如整数)是同一对象
True
>>> x = 2
>>> id(x)
31106508
>>> y = 2
>>> id(y)
31106508
>>> z = x
>>> id(x)
31106508
>>> x += 2
>>> id(x) # 只要不可变对象指向的值发生变化,不可变对象就会重新指向新的值(即4)所在的地址
31106484 
>>>x
4
>>>z  #x都已经指向了新的值所在地址,所以肯定不会影响z的值
2

内存驻留机制的影响

>>>a = "Hello_World"
>>>b = "Hello_World"
>>>a is b 
True
>>>a = "Hello World"   # 字符串中出现了非标识符时,不采用驻留,所以a与b不指向同一内存地址
>>>b = "Hello World"
>>>a is b
False
>>>a = -5
>>>b = -5
>>>a is b
True
>>>a = -6
>>>b = -6
>>>a is b
False
>>>a = 256
>>>b = 256
>>>a is b
True
>>>a = 257
>>>b = 257   # 整型的界限是(-5,256),只有这个范围内才驻留
>>>a is b
False

>>> str1='sten'+'waves'
>>> str2 is 'stenwaves'
True
>>> str3='sten'
>>> str4=str3+'waves'
>>> str4 is 'stenwaves' 
False
注:驻留适用范围:由数字,字符和下划线(_)组成的python标识符以及整数[-5,256]。 驻留时机 :python中的驻留发生在compile_time,而不是run_time。

上面这段程序都是对不可变数据类型中的int类型的操作,id()查看的是当前变量的地址值。我们先来看x = 1和y = 1两个操作的结果,从上面的输出可以看到x和y在此时的地址值是一样的,也就是说x和y其实是引用了同一个对象,即1,也就是说内存中对于1只占用了一个地址,而不管有多少个引用指向了它,都只有一个地址值,只是有一个引用计数会记录指向这个地址的引用到底有几个而已。当我们进行x = 2赋值时,发现x的地址值变了,虽然还是x这个引用,但是其地址值却变化了,后面的y = 2以及z = y,使得x、y和z都引用了同一个对象,即2,所以地址值都是一样的。当x和y都被赋值2后,1这个对象已经没有引用指向它了,所以1这个对象占用的内存,即31106520地址要被“垃圾回收”,即1这个对象在内存中已经不存在了。最后,x进行了加2的操作,所以创建了新的对象4,x引用了这个新的对象,而不再引用2这个对象。

那么为什么称之为不可变数据类型呢?这里的不可变大家可以理解为x引用的地址处的值是不能被改变的,也就是31106520地址处的值在没被垃圾回收之前一直都是1,不能改变,如果要把x赋值为2,那么只能将x引用的地址从31106520变为31106508,相当于x = 2这个赋值又创建了一个对象,即2这个对象,然后x、y、z都引用了这个对象,所以int这个数据类型是不可变的,如果想对int类型的变量再次赋值,在内存中相当于又创建了一个新的对象,而不再是之前的对象。从下图中就可以看到上面程序的过程。
在这里插入图片描述
从上面的过程可以看出,不可变数据类型的优点就是内存中不管有多少个引用,相同的对象只占用了一块内存,但是它的缺点就是当需要对变量进行运算从而改变变量引用的对象的值时,由于是不可变的数据类型,所以必须创建新的对象,这样就会使得一次次的改变创建了一个个新的对象,不过不再使用的内存会被垃圾回收器回收。

(2)可变数据类型分析。下面同样先看一段程序。

>>> a = [1, 2, 3]
>>> id(a)
1468022383176
>>> b = [1, 2, 3]
>>> id(b)
1468022383048
>>> a == b #比较的是值
True
>>> a is b #具有相同值的可变对象(例如list)是不同的对象
False
>>> c = a
>>> a.append(4)
>>> id(a)
1468022383176
>>> a += [2] #与不可变对象不同,可变对象并不是只要改变其值就会指向新地址,只有给可变对象赋新的值或者某些场景下可变对象才会指向新的地址。比如 +=就不会让可变对象指向新的地址。而 = +却会指向新地址。
>>> id(a)
1468022383176
>>> a
[1,2,3,4,2]
>>> id(c)
1468022383176
>>> c  # c会与a同步变化
[1,2,3,4,2]
>>> a = a +[9]   #a会指向新地址
>>>id(a)
1467885024648
>>>a
[1,2,3,4,2,9]
>>>c     #a与c已经不指向同一地址,所以a的变化肯定不会影响c了
[1,2,3,4,2]
>>>id(c)
 1468022383176 

从上面的程序中可以看出,进行a = [1, 2, 3] 和 b=[1,2,3] 操作,两次值相同的对象引用的地址是不同的,也就是说其实创建了两个不同的对象,这一点明显不同于不可变数据类型,所以对于可变数据类型来说,具有同样值的对象是不同的对象,即在内存中保存了多个同样值的对象,地址值不同。接着来看后面的操作,我们对列表进行添加操作,分别a.append(4)和a += [2],发现这两个操作使得a引用的对象值变成了上面的最终结果,但是a引用的地址依旧是1468022383176,也就是说对a进行的操作不会改变a引用的地址值,只是在地址后面又扩充了新的地址,改变了地址里面存放的值(注意:a += [2] 与 a = a + [2]是不同的,后者会指向新地址,*=同理。),所以可变数据类型的意思就是说对一个变量进行操作时,其值是可变的,值的变化并不会引起新建对象,即地址是不会变的,只是地址中的内容变化了或者地址得到了扩充。下图对这一过程进行了图示,可以很清晰地看到这一过程。
在这里插入图片描述

从上述过程可以看到,可变数据类型是允许同一对象的内容,即值可以变化,但是地址是不会变化的。但是需要注意一点,对可变数据类型的操作不能是直接进行新的赋值操作,比如说a = [1, 2, 3, 4, 5, 6, 7],这样的操作就不是改变值了,而是新建了一个新的对象,这里的可变只是对于类似于append、+=等这种操作。

总之,用一句话来概括上述过程就是:“python中的不可变数据类型,不允许变量的值发生变化,如果改变了变量的值,相当于是新建了一个对象,而对于相同的值的对象,在内存中则只有一个对象,内部会有一个引用计数来记录有多少个变量引用这个对象;可变数据类型,允许变量的值发生变化,即如果对变量进行append、+=等这种操作后,只是改变了变量的值,而不会新建一个对象,变量引用的对象的地址也不会变化,不过对于相同的值的不同对象,在内存中则会存在不同的对象,即每个对象都有自己的地址,相当于内存中对于同值的对象保存了多份,这里不存在引用计数,是实实在在的对象。”

python函数的参数传递

由于python规定参数传递都是传递引用,对象赋值是对象的引用,且一切都是对象,所以一切赋值都是引用,也就是传递给函数的是原变量实际所指向的内存空间(就把python中函数的参数传递理解成赋值号(=),即让形参指向实参指向的位置),修改的时候就会根据该引用的指向去修改该内存中的内容,所以按道理说我们在函数内改变了传递过来的参数的值的话,原来外部的变量也应该受到影响。但是上面我们说到了python中有可变类型和不可变类型,这样的话,当传过来的是可变类型(list,dict)时,我们在函数内部修改就会影响函数外部的变量。而传入的是不可变类型时在函数内部修改改变量并不会影响函数外部的变量,因为修改的时候会先复制一份再修改。下面通过代码证明一下:

def test(a_int, b_list):
    a_int = a_int + 1
    b_list.append('13')
    print('inner a_int:' + str(a_int))
    print('inner b_list:' + str(b_list))

if __name__ == '__main__':
    a_int = 5
    b_list = [10, 11]
    test(a_int, b_list)
    print('outer a_int:' + str(a_int))
    print('outer b_list:' + str(b_list))

运行结果如下:

inner a_int:6

inner b_list:[10, 11, '13']

outer a_int:5

outer b_list:[10, 11, '13']

所以,当函数形参是可变对象(list,dict等)时,要想在函数内部修改改变量时不影响函数外部的变量,可以给函数形参传递可变对象的拷贝,这里有深浅两种拷贝,参见下面的文章

这篇文章看完之后,可以看下面这篇,很有相关性。

NumPy:拷贝和视图

<think>好的,我现在需要回答用户关于Python可变对象不可变对象的问题。首先,我得回忆一下Python的基本数据类型,并确定它们的可变性。 我记得不可变对象包括像整数、浮点数、字符串、元组这些类型。比如,当修改字符串时,实际上是创建了一个新的字符串,原字符串不变。而可变对象比如列表、字典、集合,这些在修改时会直接改变对象本身,而不会创建新的对象。 接下来,我需要确认每个具体的数据类型是否正确分类。比如布尔类型属于不可变,因为它是基于整数的,对吧?还有frozenset应该是不可变的集合,而普通的set是可变的。这些都需要提到。 然后,用户可能需要例子来理解两者的区别。比如,列表添加元素后id不变,而字符串修改后id会变。这有助于说明可变不可变的差异。 另外,我需要解释为什么可变不可变对象在函数参数传递时表现不同,比如不可变对象作为参数时不会被函数内的操作改变,而可变对象可能会被修改。这一点对理解Python的传参机制很重要。 可能还需要提到一些注意事项,比如字典的键必须是不可变类型,所以列表不能作为键,但元组可以。这也关联到可变不可变的应用场景。 结构方面,先分类列出不可变可变对象,再举例说明,然后讨论它们的区别影响,最后总结注意事项。这样用户能逐步理解概念。 需要确保术语使用正确,比如“不可变”对应immutable,“可变”对应mutable。同时,避免使用复杂术语,保持解释清晰易懂。 可能用户会混淆某些类型,比如bytes是否不可变?是的,bytes对象也是不可变的,而bytearray是可变的。这些细节需要确认。 最后,检查是否有遗漏的类型,比如NoneType属于不可变,但用户可能更关心常见类型。所以重点放在常用的数据类型上。 总结步骤: 1. 定义不可变可变对象。 2. 列出不可变对象的具体类型。 3. 列出可变对象的具体类型。 4. 用示例说明差异。 5. 讨论对编程的影响,如参数传递、哈希等。 6. 注意事项常见误区。 现在将这些整理成结构化的回答,确保每个部分清晰,并且例子准确。</think>在Python中,对象分为**可变对象****不可变对象**,它们的核心区别在于:对象创建后是否允许修改其内部值(内存地址是否变化)。以下是具体分类说明: --- ### **一、不可变对象(Immutable)** 不可变对象一旦创建,其值不可被修改。若尝试修改,Python会创建新对象并重新分配引用。 1. **常见不可变类型** - **数值类型** - 整数:`int`(如 `5`, `-3`) - 浮点数:`float`(如 `3.14`) - 复数:`complex`(如 `1+2j`) - 布尔:`bool`(本质是 `int` 的子类,`True=1`, `False=0`) - **字符串**:`str`(如 `"hello"`) - **元组**:`tuple`(如 `(1, 2, 3)`,但若元组内包含可变对象,其内容可能间接改变) - **字节串**:`bytes`(如 `b"abc"`) - **冻结集合**:`frozenset` 2. **示例说明** ```python a = 5 # 创建整数对象5,a指向内存地址A a += 1 # 创建新整数6,a改为指向地址B print(id(a)) # 地址B ≠ 地址A ``` --- ### **二、可变对象(Mutable)** 可变对象允许在内存地址不变的情况下修改其内容。 1. **常见可变类型** - **列表**:`list`(如 `[1, 2, 3]`) - **字典**:`dict`(如 `{"a": 1}`) - **集合**:`set`(如 `{1, 2, 3}`) - **字节数组**:`bytearray` - **自定义类实例**(默认可变,除非显式限制) 2. **示例说明** ```python lst = [1, 2] # 内存地址C lst.append(3) # 修改lst内容,内存地址仍为C print(id(lst)) # 地址C不变 ``` --- ### **三、关键区别影响** 1. **赋值修改行为** - 不可变对象的赋值会复制值(或引用),修改时生成新对象。 - 可变对象的赋值传递引用,多个变量可能指向同一对象。 ```python # 不可变示例 x = "abc" y = x # xy指向同一字符串 x += "d" # x指向新字符串"abcd",y仍指向"abc" # 可变示例 a = [1, 2] b = a # ab指向同一列表 a.append(3) # ab同时变为[1, 2, 3] ``` 2. **函数参数传递** - 不可变对象作为参数时,函数内修改不会影响原对象。 - 可变对象作为参数时,函数内修改会影响原对象。 ```python def modify(num, lst): num += 1 # 不影响外部的num(不可变) lst.append(4) # 修改外部lst(可变) n = 10 my_list = [1, 2] modify(n, my_list) print(n) # 输出10(未变) print(my_list) # 输出[1, 2, 4] ``` 3. **哈希性(Hashable)** - 不可变对象可哈希(可用作字典键或集合元素)。 - 可变对象不可哈希(无法作为字典键)。 ```python d = {} d[(1, 2)] = "ok" # 元组(不可变)作为键 d[[1, 2]] = "error" # 报错!列表不可哈希 ``` --- ### **四、注意事项** 1. **元组的特殊性** 元组本身不可变,但若包含可变元素(如列表),其内部可变元素仍可修改: ```python t = ([1, 2], 3) t[0].append(3) # 允许!t变为([1, 2, 3], 3) ``` 2. **性能考量** 频繁修改不可变对象(如字符串拼接)会产生大量临时对象,此时建议使用可变类型(如列表)优化性能。 --- ### **总结** | 类型 | 不可变对象 | 可变对象 | |----------|--------------------------------|---------------------| | 典型示例 | `int`, `str`, `tuple` | `list`, `dict`, `set` | | 修改行为 | 创建新对象 | 原地修改 | | 哈希性 | 可哈希 | 不可哈希 | | 内存效率 | 修改时可能低效 | 高效 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值