变量赋值相当于什么?
Python中的变量赋值就相当于贴标签。例如创建一个列表a = [1, 2, 3],那么列表[1, 2, 3]的一个标签为a。此时将变量a赋值给变量b,也就相当于列表[1, 2, 3]有了两个标签a和b,内容和地址没有改变,只是换了个叫法。例子如下:
# 例1
a = [1, 2, 3]
b = a # 传递列表对象的引用 相当于[1, 2, 3]贴了两个标签 分别是a和b
b.append(4) # 对b操作 发现a也改变了
print(a) # [1, 2, 3, 4]
不能把b = a理解为开辟了两个不一样的空间,应该理解为a和b是同一个东西,只不过叫法不同。
内容相等和对象相同不是一回事
Python中判断两个变量的标识(可以简单理解为指向的地址,或者说引用的对象)是否相等,用的是'is',而判断两个对象的内容是否相同是用'=='号。下面的例2分别创造了一样内容的字典a和b,它们内容是相同的但是属于不同的对象。
# 例2
a = {'name': 'James', 'age': 18}
b = {'name': 'James', 'age': 18}
print(a == b) # True 内容相同 所以是对的
print(a is b) # False 因为各自创建了一个字典 所以不是相同的对象
使用'=='号的时候是在调用内置方法__eq__,用来判断两个对象的内容是否相同。这个方法是可以重写的,比如你可以重写后用'=='判断你定义的两个向量是否相等。'is'用来判断两个对象的标识是否相同,一个对象一旦创建,标识就不会改变,标识可以简单理解为创建对象的地址。使用id()函数就可以返回对象标识的整数值。所以上面的代码分别创建了两个字典(即使内容相同),标识不一样,自然a is b就是False。那如果不是各自创建,而是赋值呢?
# 例3
a = {'name': 'James', 'age': 18}
b = a # 对象引用赋值给b 那么a、b引用的内容相同、对象也相同
print(a == b) # True
print(a is b) # True
print(id(a), id(b)) # 2510206295640 2510206295640 不同的解释器可能计算的id的方式不同
对于例2,各自创建了一个字典,属于不同对象,id不同,因此互不干扰。
对于例3,只创建了一个字典,赋值使得a、b所引用的对象相同,因此改一个另一个也会随着改变。即例4:
# 例4
a = {'name': 'James', 'age': 18}
b = a # 对象引用赋值给b 那么a、b引用的内容相同、对象也相同
b['country'] = 'SZ' # 对b进行改动 发现a也变了
print(a) # {'name': 'James', 'age': 18, 'country': 'SZ'}
print(b) # {'name': 'James', 'age': 18, 'country': 'SZ'}
print(id(a), id(b)) # 1654996164184 1654996164184 对象是相同的
说句题外话,'=='号能重载,而'is'不能重载,因此通常情况下判断None用if x is None会比if x == None快。
随意赋值并改变内容的后果
既然赋值就是贴标签,那么很可能一个对象就有很多个标签,只改其中一个标签,是不是就会全部都改变呢?其实要分情况讨论…
Python的数据类型中有可变的和不可变的之分。可变的元素有列表、字典等,不可变的有数字、元组等。可变的元素意味着改变了其中的内容,它的对象还是自己。例如列表是可变的,你加进去一个数字,该列表的对象标识(id)还是原来的。不可变的元素就不一样了,元组是不可变的,一旦定义了就不能变,如果想要加一个数字进去,那么得到的必定不是原来的对象了。
复杂点来说,就是,可变的,随你怎么变还是这个对象;不可变的,一旦变了就不是同一个对象了。
举例5说明可变的对象,如果随意赋值,任意操作其中一个,牵一发而动全身,后果很可怕!
# 例5
a = [1, 2, 3] # 列表是可变对象
b = a # 传递列表对象的引用 相当于[1, 2, 3]贴了两个标签 分别是a和b
b.append(4) # 对b操作 发现a也改变了
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4]
print(id(a), id(b)) # 2286888405896 2286888405896 说明对象相同
这就警告我们,如果要复制某个列表去使用(例如增删改),回头还要使用原来的列表时,绝对不能直接用赋值(如b = a)。解决方法见下一模块。
例6说明,如果是不可变的对象,直接赋值,任意操作其中一个变量,不会影响其他的变量,后果还OK!
# 例6
a = 1 # 数字是不可变的元素
b = a # 数字1被贴上a、b的标签 a、b属于同一对象
print(a, b) # 1 1 同一对象 因此值相同
print(id(a), id(b)) # 1738370064 1738370064 id相同 还是同一对象
b = 2 # 对不可变的元素更改内容 会导致重新开辟空间 产生新的对象
print(a, b) # 1 2
print(id(a), id(b)) # 1738370064 1738370096 可见不可变的元素更改后与原对象不同
不可变对象赋值后改变就相当于拷贝另一份了,之后就互不相干了,因此看上去还算比较安全,不会改一个就全都改了,但是后果还OK!不是完全OK,因为改一次你就会重新开辟新的空间,创建新的对象,因此蛮费空间的。
安全赋值怎么操作?
上面讲到可变元素赋值的时候,多个变量改其中一个其他也会变,那么怎么避免呢?如果赋值后还需要用到原来的对象,那么建议进行拷贝操作,也就是复制一份。
复制一份的操作也要分情况讨论。如果可变元素的内容全部是不可变的时候,那就直接拷贝,例如一个列表(可变)内都是整数(不可变)。
# 例7
a = [1, 2, 3] # 列表是可变的 内部元素是整数 不可变
b = list(a) # 拷贝生成新的列表对象
c = a[:] # 拷贝生成新的列表对象
print(a, b, c) # [1, 2, 3] [1, 2, 3] [1, 2, 3]
print(id(a), id(b), id(c)) # 2099645172616 2099646935176 2099646889736 对象不同 互不相干
如果可变元素的内容包含可变的元素,例如列表里包含列表,如[1, 2, [3, 4]],或是不可变元素里包含可变的,例如元组里面含有列表,如(1, 2, [3, 4]),那么上述的拷贝方法也存在安全隐患。换句话说,遇到嵌套在里面的可变元素,普通拷贝还是有隐患。
浅拷贝和深拷贝
Python的字典、元组、列表等都是容器序列,他们不直接存放值,而是存放里面对象的引用。比如a = [1, 2, [3, 4]],列表a中的第三个元素是[3, 4],列表a内存中存放的并不是3, 4的值,而是列表[3, 4]对象的引用。但是直接拷贝这个列表时,如使用b = list(a),或者b = a[:],得到的是浅拷贝。浅拷贝字面理解就是拷贝的不充分,只是复制了元素的对象的引用,用上面的例子就是指只有拷贝了1,2和列表[3, 4]的对象的引用(整型数据也有对象的引用)。前面我们知道,对于1,2是不可变元素,所以你一旦改变了就会创建新的空间存储,标识就会改变,因此不是原来的对象,但是对于列表[3, 4],就算你改变了列表[3, 4]里面元素的内容,他的对象的引用还是不会变,因此存在风险。
# 例8
import copy
a = [1, 2, [3, 4]] # 可变的列表嵌套着另一个可变的列表
b = copy.copy(a) # 类似于b = a[:]的浅拷贝
print(a, b) # [1, 2, [3, 4]] [1, 2, [3, 4]] 浅拷贝一份 内容一模一样
print(id(a), id(b)) # 2797099397192 2797099399240 外层容器复制了一份 因此对象不同
print(id(a[2]), id(b[2])) # 2797099351880 2797099351880 a、b内部存储的是列表[3, 4]对象的引用 因此浅拷贝后对象的引用没变
b[2].append(5) # 对b中的列表[3, 4]加一个数字5
print(a, b) # [1, 2, [3, 4, 5]] [1, 2, [3, 4, 5]] 对b[2]操作 结果a[2]却变了 说明浅拷贝后改内部的可变元素也会改变原来的拷贝前的对象的元素
print(id(a), id(b)) # 2797099397192 2797099399240 外部列表还是原来各自不同的id
print(id(a[2]), id(b[2])) # 2797099351880 2797099351880 内部的列表还是原来相同的id
同理,哪怕是不可变的元组如a = (1, 2, [3, 4]),里面嵌套一个可变的列表即[3, 4],浅拷贝后,改其中一个元组内的可变元素,比如往[3, 4]里加一个数字,也会使得拷贝的元组同样发生改变。这就是元组的相对不可变性,元组的定义是说不可变的,是指保存的对象的引用不可变,与引用的内容无关,所以元组内部的元素是可能改变的。用上面的例子就是元组里面的列表[3, 4]对象的引用不可变,但是列表[3, 4]里面的内容不管多一个数少一个数,不考虑。
浅拷贝有隐患,就相当于拷贝的不充分,只拷贝到了内部元素的对象的引用,但是对象引用的内容更改还是没能独立完成。那么就有了深拷贝的出现,深拷贝不仅仅是拷贝到内部元素的对象的引用,而是接着去拷贝到每一个元素内容,就相当于完全复制一份,不管是否内部包含可变元素了。深拷贝安全,但是比较费空间。
# 例9
import copy
a = (1, 2, [3, 4]) # 不可变的元组嵌套着另一个可变的列表
b = copy.deepcopy(a) # 深拷贝 a、b完全不相关了
print(a, b) # (1, 2, [3, 4]) (1, 2, [3, 4]) 深拷贝一份 内容一模一样
print(id(a), id(b)) # 2531887773736 2531887842216 外层容器复制了一份 因此对象不同
print(id(a[2]), id(b[2])) # 2531887873864 2531887921224 内部存储的是列表[3, 4]对象的引用 深拷贝后不同了 相互独立
b[2].append(5) # 对b内的列表[3, 4]加一个数字5
print(a, b) # (1, 2, [3, 4]) (1, 2, [3, 4, 5]) 对b[2]操作 a[2]不变 说明深拷贝后各自独立
print(id(a), id(b)) # 2531887773736 2531887842216 外部元组还是原来各自不同的id
print(id(a[2]), id(b[2])) # 2531887873864 2531887921224 内部的元组还是原来各自不同的id