携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情 >>
2.2 不变的情况
与数值不同,Python 中对列表对象的操作还表现出另一种特性。考虑下面的代码:
>>> c = [1, 2, 3]
>>> id(c)
2161458355400
>>> c[2] = 5
>>> c
[1, 2, 5]
>>> id(c)
2161458355400
>>> c.append(3)
>>> c
[1, 2, 5, 3]
>>> id(c)
2161458355400
观察代码块第 3、8、13三行,输出相同。也就是说,对于列表而言,可以通过直接操作变量本身,从而在不改变其引用的情况下改变所引用的值。
更进一步地,如果是两个变量同时引用同一个列表,则对其中一个变量本身直接进行操作,也会影响到另一个变量的值:
>>> c = [1, 2, 3]
>>> cc = c
>>> id(c)
1823864610120
>>> id(cc)
1823864610120
显然此时的变量c和cc的id是一致的。现在改变c所引用的列表值:
>>> c[2] = 5
>>> cc
[1, 2, 5]
可以看到cc所引用的列表值也随之变化了。再看看相应地id:
>>> id(c)
1823864610120
>>> id(cc)
1823864610120
两个变量的id都没有发生变化。再调用append()方法:
>>> c.append(3)
>>> c
[1, 2, 5, 3]
>>> cc
[1, 2, 5, 3]
>>> id(c)
1823864610120
>>> id(cc)
1823864610120
删除元素:
>>> del c[3]
>>> c
[1, 2, 5]
>>> cc
[1, 2, 5]
>>> id(c)
1823864610120
>>> id(cc)
1823864610120
在上述所有对列表的操作中,均没有改变相应元素的引用。
也就是说,对于变量本身进行的操作并不会创建新的对象,而是会直接改变原有对象的值。
2.3 一个特殊的地方
数值数据和列表还存在一个特殊的差异。考虑如下代码:
>>> num = 10000
>>> id(num)
2161457772336
>>> num += 1
>>> id(num)
2161457774512
有了前面的铺垫,这样的结果很显得很自然。显然在对变量num进行增1操作的时候,还是计算出新值然后进行赋值操作,因此引用发生了变化。
但列表却不然。见如下代码:
>>> li = [1, 2, 3]
>>> id(li)
2161458469960
>>> li += [4]
>>> id(li)
2161458469960
>>> li
[1, 2, 3, 4]
注意第 4 行。明明进行的是“相加再赋值”操作,为什么有了跟前面不一样的结果呢?检查变量li的值,发现变量的值也确实发生了改变,但引用却没有变。
实际上这是因为加法运算符在 Python 中存在重载的情况,对列表对象和数值对象来说,加法运算的底层实现是完全不同的,在简单的加法中,列表的运算还是创建了一个新的列表对象;但在简写的加法运算+=实现中,则并没有创建新的列表对象。这一点要十分注意。
3. 原理解析
前面(第3天:Python 变量与数据类型[8])我们提到过,Python 中的六个标准数据类型实际上分为两大类:可变数据和不可变数据。其中,列表、字典和集合均为“可变对象”;而数字、字符串和元组均为“不可变对象”。实际上上面演示的数值数据(即数字)和列表之间的差异正是这两种不同的数据类型导致的。
由于数字是不可变对象,我们不能够对数值本身进行任何可以改变数据值的操作。因此在 Python 中,每出现一个数值都意味着需要另外分配一个新的内存空间(常量池中的数值例外)。
>>> a = 10000
>>> a == 10000
True
>>> a is 10000
False
>>> id(a)
2161457773424
>>> id(10000)
2161457773136
>>>
from sys import getrefcount
>>> getrefcount(a)
2
>>> getrefcount(10000)
3
前 9 行的代码容易理解:即使是同样的数值,也可能具有不同的引用值。关键在于这个值是否来自于同一个对象。
而第 10 行的代码则说明除了getrefcount()函数的引用外,变量a所引用的对象就只有1个引用,也就是变量a。一旦变量a被释放,则相应的对象引用计数归零,也会被释放;并且只有此时,这个对象对应的内存空间才是真正的“被释放”。
而作为可变对象,列表的值是可以在不新建对象的情况下进行改变的,因此对列表对象本身直接进行操作,是可以达到“改变变量值而不改变引用”的目的的。
4. 总结
对于列表、字典和集合这些“可变对象”,通过对变量所引用对象本身进行操作,可以只改变变量的值而不改变变量的引用;但对于数字、字符串和元组这些“不可变对象”,由于对象本身是不能够进行变值操作的,因此要想改变相应变量的值,就必须要新建对象,再把新建对象赋值给变量。