且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

古灵精怪的python——地址,浅拷贝与身份运算符

更新时间:2022-01-31 10:45:21

首先抛出一个问题,吸引读者的阅读兴趣(如果您觉得这个不是问题,那么这篇文章不适合您:)

请看如下代码:

>>> a = 3

>>> b = 3
>>> a == b

True
>>> a is b
True
>>> b = a

# 这没问题
>>> a = 3
>>> a == b
# 这看起来也很合理

True
>>> a is b
True
>>> a is b

>>> a = (2,3)
>>> b = (2,3)
>>> b = a

False # ???why?
>>> a == b
True
>>> a is b
True

True # ???why?
>>> a == b

好了,整篇文章都是围绕这个问题展开的。长久以来我都习惯用is而不用==来进行两个对象的比较(python中一切皆对象) 直到今天出了一个bug后才了解到这两者之间的不同,挖到python的一个大坑之余,不禁出了一身冷汗。。。

is还是==

补充知识

id() 用于获取对象在内存中的地址,并以十进制展示出来。如:

>>> a = 3

>>> id(a)
140602638349720

>>> hex(id(a)) # 还原成我们看着更顺眼的16进制,但是本文以10进制地址为主(因为懒)

'0x7fe09a503598'

顾名思义,is是“相同”,而==是指两者之间的”相等“关系。所谓相同,比较的是两者之间的在内存中的位置,

>>> a = 3

>>> id(a)
140602638349720

>>> b = 3 # b指向的是和a指向的同一块地址(但是并不意味这改变了a,b也会相应改变)

>>> id(b)
140602638349720
140602638349720

>>> c = a # a的引用复制给c,在内存中其实是指向了用一个对象
>>> id(c)
>>> a is b
True
True

>>> a is c
True
>>> b is c

我们看到,上面a,b,c的地址相同,所以他们互相之间”相同“

而相等则两者之间的数值对应相等

>>> a = 3

>>> b = a
>>> b

>>> a = 4
3
>>> b = [3]

>>> a = [3]
>>> id(a)
4351374112

4351374184
>>> id(b)
>>> a is b
>>> a[0] = 4

False 
>>> a == b
True
>>> b
[3]
>>> b = a # b就是a的引用,占得是同一块地址,而且当a的内容改变时,b也会随之改变,这和上面

>>> a = [3]
# int对象不同,我也不知道为啥要这么搞。
[4] 

>>> a[0] = 4
>>> b

很多同学看到这肯定是一锅浆糊了,其实就是一个原则,能用==就不用is。除了一种情况,那就是判断对象是否是None。

>>> if a is None:

... pass

浅拷贝和深拷贝

>>> a = [3]

>>> b = a[:] #通过切片赋值,返回的是a的浅拷贝

>>> id(a)
4351273944
>>> id(b)
140602638349720

4351374184
>>> id(a[0])
>>> a is b 

>>> id(b[0]) #list的地址不同
140602638349720
False
>>> a[0] is b[0] #浅拷贝,只拷贝了a的壳[],里边的内容仍然是同一个东西,同样的id

True
>>> a == b
True
>>> a[0] == b[0]
True
>>> a[0] = 4 # 但是,b的内容不会随着a的变化而变化
>>> b
[3]

浅拷贝拷贝了最外层容器,副本中的元素是原容器中元素的引用

我们再看一个例子

>>> Anndy = ['Anndy', ['age', 24]]

>>> Tom = Anndy[:]
>>> id(Anndy)

>>> Cindy = list(Anndy)
4351374040
4351374616

>>> id(Tom)
4351373968
>>> id(Cindy)
(['Anndy', ['age', 24]],['Anndy', ['age', 24]],['Anndy', ['age', 24]])

>>> print(Anndy, Tom, Cindy)
# 看起来是创建了三个不同的对象,因为他们的id各不相同
>>> print (Anndy, Tom, Cindy)

>>> Tom[0] = 'Tom'
>>> Cindy[0] = 'Cindy'
# 如果想修改某一个人的名字也没有什么问题

(['Anndy', ['age', 24]], ['Tom', ['age', 24]], ['Cindy', ['age', 24]])
# 现在我们想把Tom的年龄修改为12岁
>>> Tom[1][1] = 12
# 震惊!所有人的年龄都变成了12!!!

>>> print (Anndy, Tom, Cindy)
(['Anndy', ['age', 12]], ['Tom', ['age', 12]], ['Cindy', ['age', 12]])
>>> print ([id(x) for x in Anndy])
[4351366224, 4351374112] # 第一个姓名元素的地址不同,但是第二个列表是同一个

[4351366368, 4351374112] # 看第二个列表的地址
>>> print ([id(x) for x in Tom])
[4351323592, 4351374112] # 看第二个!
>>> print ([id(x) for x in Cindy])

构造方法或切片 [:] 做的是浅拷贝。如果所有元素都是不可变的(比如名字字符串,修改的时候会重新创建对象,仅仅包括原子对象的元组也属于这种情况),那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题,正如刚刚,修改一个人的年龄,所有人的年龄都发生了变化。

所以,如果你想要深拷贝,应该这么写

>>> import copy

>>> Anndy = ['Anndy', ['age', 24]]

>>> Tom = copy.deepcopy(Anndy)
>>> print(Tom, Anndy)

>>> Tom[1][1] = 12
(['Anndy', ['age', 12]], ['Anndy', ['age', 24]]) #这样写就没问题了

另外

不知道刚才你有没有注意到

>>> a = 3

>>> b = 3
140602638349720

>>> id(a)
>>> id(b)
140602638349720 # 相同!

>>> a is b
True

Python会对比较小的整数对象进行缓存缓存起来。当整数比较大的时候就会重新开辟一块内存。

>>> a = 999

>>> b = 999
140602638469952

>>> id(a)
>>> id(b)
>>> a is b

140602638469904 # 不同!
False

这仅仅是在命令行中执行,而在保存为文件执行,结果是不一样的,这是因为解释器做了一部分优化。

#!/usr/bin/env python

a = 3
b = 3
a = 99999

print(a is b)
b = 99999
True

print(a is b)
结果:
True
[Finished in 0.0s]

这也是为什么我屡屡用is而不用==,程序运行良好的原因。

总结

1. python中,尽量不要用is, 除非判断对象是否为None

2. a is b(相同)一定意味着a == b(相等),而a == b(相等) 不一定 a is b (相同)这点比较好理解

3. 如果函数中传参等,需要引用、拷贝的,注意是否生成了一个新的对象,即使生成了,内部元素是否是同一个对象的引用?尤其注意切片的使用。必要的时候用copy模块进行深拷贝而不要用切片这种浅拷贝形式。


原文发布时间为:2018-08-12

本文作者:DeepWeaver

本文来自云栖社区合作伙伴“Python爱好者社区”,了解相关信息可以关注“Python爱好者社区”。