关于Python中的可变类型、不可变类型、深浅拷贝以及小整数池与驻留机制的总结
0.前言
对于从c/c++转到Python的人来说,可能会有很多的不适应,除格式上的巨大区别外,如变量的地址、弱类型、各种推导式等。而对于一个学习过c++的人来说,赋值常常是指传值,但在Python中,很多情况都是传引用,我们以下列代码引出今天的问题(来源于知乎)
a = {"num":1}
list1 = []
for i in range(10):
a["num"] = i
list1.append(a)
print(list1)
结果如下
[{'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}, {'num': 9}]
而当代码改成下列时
a = {"num":0}
list1 = []
for i in range(10):
a = {"num":i}
list1.append(a)
print(list1)
结果就会变成
[{'num': 0}, {'num': 1}, {'num': 2}, {'num': 3}, {'num': 4}, {'num': 5}, {'num': 6}, {'num': 7}, {'num': 8}, {'num': 9}]
对于从c/c++转过来的人,通常认为第一个代码也应该输出第二种结果,如同c中的数组一样,但这是由于Python的赋值机制以及序列的存储机制造成的,下面我们一一来解析
1.可变类型与不可变类型
Python的数据类型可分为两种,可变类型和不可变类型
- 不可变类型:
int
整型、float
浮点型、string
字符串型、tuple
元组 - 可变类型:
set
集合、list
列表、dict
字典
不可变类型
不可变类型是指变量若被修改,其地址也会发生变化;即地址上的内容是不可变的,不可变类型的修改是通过修改地址来完成的,即创建一个新的对象,再让变量指向这个新的对象
如下列代码
a = 1
print(id(a))
a += 1
print(id(a))
4317505856
4317505888
a = '1'
print(id(a))
a += '1'
print(id(a))
4389343896
4370932912
元组无法修改,所以不做展示
可变类型
可变类型是指在修改变量时其地址不会发生变化
值得注意的是,重新赋值这一操作并非修改,而是创建一个新的变量,所以地址也会发生变化,这点不在我们的讨论范围之内
set1 = {1,2,3,"123"}
print(id(set1))
set1.add(1)
print(id(set1))
4334929728
4334929728
list1 = [1,2,3]
print(id(list1))
list1.append(1)
print(id(list1))
4373599680
4373599680
dict1 = {1:1,2:2}
print(id(dict1))
dict1[1] = 2
print(id(dict1))
4308701696
4308701696
2.深浅拷贝
在数据传递的过程中,可能会发生数据被修改的情况,为了防止数据被修改,通常需要传递一个副本,这样,副本的修改不会影响到原数据,为了生成这个副本,就产生了拷贝
赋值运算
list1 = [1,2,3]
list2 = list1
list2.append(4)
print(list1,list2)
[1,2,3,4]
[1,2,3,4]
其中,赋值语句是一个指向,即传引用,与拷贝无关
我们发现,这样的赋值会使得修改互相影响,通常这是不安全的
浅拷贝
浅拷贝仅仅复制对象的各种数据,不会直接指向复制对象,可使用.copy()
方法或者是copy.copy()
方法来实现
list1 = [1,2,3]
list2 = list1.copy()
list1.append(1)
此时list1
被修改,但是list2
不会被修改
但是由于列表中只是存储地址,有些情况下,修改不可变序列也会影响到浅拷贝的对象
list1 [1,2,3,[1]]
list2 = list1.copy()
list1.[-1].append(1)
print(list2) #[1,2,3,[1,1]]
在浅拷贝中,列表的拷贝也仅仅是创建一个新空间,将原本列表中排列着的地址存入,其中存储的可变数据地址仍然共用
深拷贝
对于上述问题,我们可以引入深拷贝来解决
import copy
list1 [1,2,3,[1]]
list2 = copy.deepcopy(list1)
list1.[-1].append(1)
print(list2) #[1,2,3,[1]]
深拷贝可将拷贝的可变序列也复制一份,放入新的地址中,不可变序列则沿用之前的
由于浅拷贝效率更高,除非特殊需要,否则不用深拷贝
小整数池以及驻留机制
在Python中,不同变量的地址不同,即使是重复声明相同的变量
但是当两个不可变类型的变量声明在同一行时,为了优化,其地址也相同
而在如今许多编译器中,通过优化,不可变类型的变量即使不在一行声明,其地址也会相同,所以以下测试都在终端命令行中进行
小整数池机制
为了效率,Python中小整数仅仅会在内存中存放一份,所以其地址是一样的,其中小整数的范围是 [ − 5 , 255 ] [-5,255] [−5,255],这就是Python的小整数池机制
只有整数存在这种情况,小数则不存在
字符串驻留机制
同小整数池机制一样,字符串也有类似的机制。对于仅仅由数组、下划线、字母组成的字符串,内存也会只保存一份
但注意的是,驻留时机是在编译的时候驻留的,不是在运行的时候驻留的
在编译时期,c
的值不能被确定,只有在运行时才能被确定,所以不会驻留
所以在Python中对字符串的改动会采用重新创建新对象的办法,且原字符串会被驻留,因此不推荐使用+
来拼接字符串,推荐使用join
函数
join
一次性计算好最终需要的内存空间,再把字符串存入
而+
则是每次把左右两个字符串都复制到一个新空间