可变对象与不可变对象
python中对象有两种类型:
- 可变对象类型:list、dict、set
- 不可变对象类型:tuple、string、int、float、bool
我们可以简单理解为:
- 可变对象类型的变量中记录是:对象的地址值
- 不可变对象类型的变量中记录时:对象的值
可变对象类型变量 list1 和 list2 可以通过相同地址值,指向同一个对象
list1 = [1, 2, 3, 4, 5]
list2 = list1 # 将list1的地址值赋给list2,使得list1,list2指向同一个对象
list1[0] = 2 # list1 修改对象内容
print(list1)
print(list2) # list2 和 list1 指向的是同一个对象,因此list2打印结果和list1相同
但是不可变对象类型的变量,不具备上面特点
num1 = 10
num2 = num1
num1 -= 1
print(num1) # 9
print(num2) # 10
生成含 x 个 不可变对象 的列表
列表乘法
list1 = [0] * 2
列表推导式
list2 = [0 for _ in range(2)]
由于 0 是不可变对象,因此这两种方式生成的列表没有区别。
生成含 x 个 可变对象 的列表
列表乘法
list1 = [[]] * 2
列表推导式
list2 = [[] for _ in range(2)]
[]列表,属于可变对象,此时上面两种方式生成的列表是有区别的
list1 = [[]] * 2
list2 = [[] for _ in range(2)]
print(list1) # [[], []]
print(list2) # [[], []]
list1[0].append(0)
list2[0].append(0)
print(list1) # [[0], [0]]
print(list2) # [[0], []]
通过运行上面代码,我们可以发现,list1的打印结果似乎不太对劲。我们只是向 list1[0] 中追加了一个数值0,但是list1[1] 也被追加了一个数值0。
而 list2 没有发生这样的问题。
列表类型属于可变对象类型,列表类型的变量本质记录的是对象的地址值,我们可以使用id函数来获取对象的地址值,因此我们可以将 list1 和 list2 内部元素的地址值打印出来看看
list1 = [[]] * 2
list2 = [[] for _ in range(2)]
print(list(map(id, list1))) # [2779372310784, 2779372310784]
print(list(map(id, list2))) # [2779372317376, 2779372633152]
可以发现,list1 中两个元素的对象地址值是相同的,而 list2 中两个元素的对象地址值是不相同的。
也就是说,list1中虽然有两个元素,但是这两个元素的对象地址值相同,即指向同一个对象,就好比两条绳子(变量)A,B牵着一头牛(对象),你通过绳子 A 把牛拽过来给它挂了个铃铛后,你再通过绳子 B 再把牛拽过来,发现它就是刚刚被挂铃铛的牛。
牛是同一头牛,但是有两条绳子牵着它。你无论通过哪个绳子拽牛,拽来的都是同一头牛。
也就说,列表乘法不会生成新的牛,只是生成了牵着同一头牛的多条绳子。
那么列表推导式,是会生成新的牛吗?我们不妨再看下面代码:
ele = []
list2 = [ele for _ in range(2)]
print(list2) # [[], []]
list2[0].append(0)
print(list2) # [[0], [0]]
此时,我们发现 list2 发生之前相同的问题。我们打印此时 list2 内部元素的地址值,发现内部两个元素也指向了同一个对象。
ele = []
list2 = [ele for _ in range(2)]
print(list(map(id, list2))) # [1849845272832, 1849845272832]
造成问题出现的原因是,list2 的生成代码做了如下改变:
list2 = [[] for _ in range(2)]
变为了
ele = []
list2 = [ele for _ in range(2)]
list2 = [[] for _ in range(2)] 给了我们一种错觉,似乎这种方式可以产生新的对象,但是实际上,列表推导式只是一种语法糖,它只是对 for 循环的写法上的简化,你可以认为列表推导式:
list2 = [[] for _ in range(2)]
等价于
list2 = [] for _ in range(2): list2.append([])
其中 [] 其实是一种动作,表示:创建一个空列表,比如:
- list2 = [],表示创建一个空列表后,赋值给 list2
- list2.append([]),表示创建一个空列表后,追加到 list2 尾部
那么:
ele = [] list2 = [ele for _ in range(2)]
其实可以认为等价于
ele = [] list2 = [] for _ in range(2): list2.append(ele)
上面代码中
- ele = [],表示创建了一个空列表赋值给 ele
- list2.append(ele),表示将 ele的地址值 追加到 list2 尾部
这种方式本质和列表乘法并无区别。
总结
列表乘法 [x] * n,本质是将要元素 x 复制 n 次。
若要重复的元素 x 是不可变对象类型,则使用列表乘法后,会将元素 x 的值复制多次,最终生成的列表中的每个元素:值相同,但是不是同一个对象。
若要重复的元素 x 是可变对象类型,则使用列表乘法后,会将元素 x 的对象地址值复制多次,最终生成的列表中元素:地址值相同,都指向同一个对象。
列表推导式,本质是一种语法糖,它只是简化了 for 循环写法。在代码理解上,我们还是应该将列表推导式当成普通 for 循环来解读。