Python - “列表乘法”和“列表推导式”区别

可变对象与不可变对象

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 循环来解读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员阿甘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值