前言
Python表达式中i += x
和i = i +x
是否等价?一般理解它们是等价的,整数操作时候它们没有什么区别,但对于列表操作,就大为不同了,先看一段代码
l1 = list(range(3))
l2 = l1
l2 += [3]
print("l1=", l1)
print("l2=", l2)
输出:
l1= [0, 1, 2, 3]
l2= [0, 1, 2, 3]
l1 = list(range(3))
l2 = l1
l2 = l2 + [3]
print("l1=", l1)
print("l2=", l2)
输出:
l1= [0, 1, 2]
l2= [0, 1, 2, 3]
为什么上下两段代码结果会不一样呢,弄清楚这个问题之前首先得明白两个概念:
- 可变对象
- 不可变对象
可变对象和不可变对象
在Python中任何对象都有三个通用的属性:
- 唯一标识 用于标识对象在内存中唯一性,它在对象创建之后就不会再改变,函数
id()
可以查看对象的唯一标识 - 类型 类型决定了该对象支持哪些操作,不同的类型支持的操作就不一样,例如列表可以有length属性,而整数没有。同样的,对象类型一旦确定就不会再变,除非重新指定,
type()
可以返回对象的类型属性 - 值 对象的值与唯一标识不一样,并不是所有的对象的值都是一成不变的,有些对象可以通过某些操作发生改变。
- 可变对象: 值可以变化的对象
- 不可变对象:值不可以发生变化的对象,例如
int、tuple、set、str
不可变对象
对于不可变对象,值永远是刚开始创建时候的值,对该对象做的任何操作都会导致一个新的对象的创建
a = 1
print(id(a))
a += 1
print(id(a))
输出:
94070312147488
94070312147520
- 整数
1
是不可变对象,最初复制的时候,变量(标签)a
指向了整数对象1
- 当对对象执行
+=
操作后,a
指向了另一个整数对象2
- 但对象
1
还在那里没有发生任何变化,而变量a
已经指向了一个新的对象2
可变对象
- 可变对象的值可以通过某些操作动态地调整改变,例如列表对象,可以通过
append
方法不断地往列表中添加元素,该列表值就在不断地处于变化中 - 一个可变对象赋值给两个变量时,它们共享同一个实例对象,指向相同的内存地址
- 因此对任何一个变量操作时,都会影响另外一个变量
x = list(range(3))
y = x
x.append(1)
print("type(x)=", type(x))
print("id(x)=", id(x))
print("id(y=", id(y))
输出:
type(x)= <class 'list'>
id(x)= 139977409416904
id(y= 139977409416904 # id是相同的
执行append
操作后,内存的地址不会改变,x、y依然指向同一个对象,只不过是它的值发生了变化而已
总结
因此回头来看不可变对象,浅拷贝=
id是相同的:
a = 1
b = a
print("id(a)", id(a))
print("id(b)", id(b))
输出:
id(a) 93938262067744
id(b) 93938262067744 # id相同
但修改的时候,id
是不同的:
a = 1
b = a
a = a + 1
print("id(a)", id(a))
print("id(b)", id(b))
输出:
id(a) 94019457841728
id(b) 94019457841696 # 一旦它对数据进行修改,就会新建出一个对象,id不同
但修改可变对象的时候,id
不会发生改变:
x = list(range(3))
y = x
x.append(1)
print("type(x)=", type(x))
print("id(x)=", id(x))
print("id(y=", id(y))
输出:
type(x)= <class 'list'>
id(x)= 139977409416904
id(y)= 139977409416904 # 即使修改,id也是相同的,因此值相同
不可变对象和可变对象的浅拷贝与深拷贝
不可变对象的浅拷贝和深拷贝是一样的,id
相同,甚至说没有浅拷贝和深拷贝的概念,因为其对值修改,都会重新创建一个对象,然后另变量等于它
不可变对象的浅拷贝
a = (1,2,3)
b = a
print("id(a)=", id(a))
print("没有修改之前的id(b)=",id(b))
b = b*3
print("id(a)=",id(a))
print("修改之后的id(b)=", id(b))
输出:
id(a)= 139638639777328
没有修改之前的id(b)= 139638639777328
id(a)= 139638639777328
修改之后的id(b)= 139638648537760 # 发生了改变
不可变对象的深拷贝
import copy
a = (1,2,3)
b = copy.deepcopy(a)
print("id(a)=", id(a))
print("没有修改之前的id(b)=",id(b))
b = b*3
print("id(a)=",id(a))
print("修改之后的i输出d(b)=", id(b))
可变对象的浅拷贝
=
浅拷贝——值相等,地址相等
a = [1,2,3]
b = a
print("id(a)=", id(a))
print("没有修改之前的id(b)=",id(b))
b.append(4)
print("id(a)=",id(a))
print("修改之后的i输出d(b)=", id(b))
print("a=", a)
print("b=", b)
输出:
id(a)= 140222374154760
没有修改之前的id(b)= 140222374154760
id(a)= 140222374154760
修改之后的i输出d(b)= 140222374154760
a= [1, 2, 3, 4]
b= [1, 2, 3, 4]
copy
浅拷贝——值相等,地址不等——对象的值是不可变对象
- 先看一个对象的值中是 不可变对象,当对象的值是可变对象时候,浅拷贝和深拷贝是不同的
import copy
a = [1,2,3]
b = copy.copy(a)
print("id(a)=", id(a))
print("没有修改之前的id(b)=",id(b))
b.append(4)
print("id(a)=",id(a))
print("修改之后的i输出d(b)=", id(b))
print("a=", a)
print("b=", b)
输出:
id(a)= 139704283594568
没有修改之前的id(b)= 139704283593992
id(a)= 139704283594568
修改之后的i输出d(b)= 139704283593992
a= [1, 2, 3]
b= [1, 2, 3, 4] # 因此b发生改变之后,a不会发生变化
可变对象的deepcopy
深拷贝——值相等,地址不等及其与copy
的区别
- 既然都是值相等,地址不等,那么和
copy
浅拷贝有什么区别呢?
对可变对象而言,对象的值一样可能包含有对其他对象的引用。浅拷贝产生的新对象,虽然具有完全不同的id,但是其值若包含可变对象,这些对象和原始对象中的值包含同样的引用,一旦对其进行修改,那么相应的也会发生修改
copy
浅拷贝——-对象的值是 可变对象 拷贝不彻底
import copy
l = {'a': [1, 2, 3], 'b': [4, 5, 6]}
c = copy.copy(l)
print("修改前id(l)=", id(l))
print("修改前id(c)=", id(c))
l['a'].append("a")
print("修改后id(l)=", id(l))
print("修改后id(c)=", id(c))
print('l=', l)
print("c=", c)
输出:
修改前id(l)= 140495444294752
修改前id(c)= 140495444294608
修改后id(l)= 140495444294752
修改后id(c)= 140495444294608
l= {'a': [1, 2, 3, 'a'], 'b': [4, 5, 6]}
c= {'a': [1, 2, 3, 'a'], 'b': [4, 5, 6]} # 虽然id不同,但是引用对象是可变对象时候,一旦引用对象发生了变化,其也会跟着发生变化
c = list[:]
列表有一种特殊的拷贝方式,这种拷贝方式与
copy
浅拷贝相同a = [1, 2, 3] l = [a, [4, 5, 6]] c = l[:] print("修改前id(l)=", id(l)) print("修改前id(c)=", id(c)) c[0].append('a进行了修改') print("修改后id(l)=", id(l)) print("修改后id(c)=", id(c)) print('l=', l) print("c=", c) 输出: 修改前id(l)= 140175922401096 修改前id(c)= 140175922401160 修改后id(l)= 140175922401096 修改后id(c)= 140175922401160 l= [[1, 2, 3, 'a进行了修改'], [4, 5, 6]] c= [[1, 2, 3, 'a进行了修改'], [4, 5, 6]]
而如果不是对引用对象是可变对象进行修改,则:
a = [1, 2, 3] l = [a, [4, 5, 6]] c = l[:] print("修改前id(l)=", id(l)) print("修改前id(c)=", id(c)) c.append('a进行了修改') print("修改后id(l)=", id(l)) print("修改后id(c)=", id(c)) print('l=', l) print("c=", c) 输出: 修改前id(l)= 140003322496520 修改前id(c)= 140003304967816 修改后id(l)= 140003322496520 修改后id(c)= 140003304967816 l= [[1, 2, 3], [4, 5, 6]] c= [[1, 2, 3], [4, 5, 6], 'a进行了修改']
deepcopy
——对象的值是可变对象时候
deepcopy
会递归地查找对象中包含的其他对象的引用,来完成更深层次的拷贝。因此,深拷贝产生的副本可以随意修改而无需担心会引起原始值的改变
import copy
l = {'a': [1, 2, 3], 'b': [4, 5, 6]}
c = copy.deepcopy(l)
print("修改前id(l)=", id(l))
print("修改前id(c)=", id(c))
l['a'].append("a")
print("修改后id(l)=", id(l))
print("修改后id(c)=", id(c))
print('l=', l)
print("c=", c)
输出:
修改前id(l)= 140429251315536
修改前id(c)= 140429251315752
修改后id(l)= 140429251315536
修改后id(c)= 140429251315752
l= {'a': [1, 2, 3, 'a'], 'b': [4, 5, 6]}
c= {'a': [1, 2, 3], 'b': [4, 5, 6]} # c不会跟着发生变化
+=
与+
的区别
那么回过头来,两种赋值方法有什么区别呢?+=
操作会尝试调用__iadd__
方法,如果没有该方法,则会调用__add__
方法
而+
方法直接会调用__add__
方法
__add__
和__iadd__
的区别
__add__
方法接受两个参数,返回它们的和,两个参数的值均不会发生改变__iadd__
方法接受两个参数,但它属于in-place
操作,就是说它会改变第一个参数的值,因为这需要对象是可变的,所以对于不可变对象,其没有__iadd__
方法
显然,整数对象没有__iadd__
方法,而列表对象提供了__iadd__
方法,来看两者的区别,回到前面的代码:
l1 = list(range(3))
l2 = l1
l2 += [3]
print("l1=", l1)
print("l2=", l2)
输出:
l1= [0, 1, 2, 3]
l2= [0, 1, 2, 3]
l1 = list(range(3))
l2 = l1
l2 = l2 + [3]
print("l1=", l1)
print("l2=", l2)
输出:
l1= [0, 1, 2]
l2= [0, 1, 2, 3]
- 代码1中的
+=
操作调用的是__iadd__
方法:接受两个参数,它会原地修改第一个参数的值l2 += [3]
,也就是l2
指向的那个对象本身的值
代码2中
+
操作调用的是__add__
方法,该方法返回一个新的对象,原来的对象保持不变,l1
还是指向原来的对象,而l2
指向了一个新的对象
实践
来看一个之前可以称为玄学的bug:
a = [1, 2, 3]
b = [4, 5, 6]
for i in [a, b]:
"""
0. 这里执行的是__add__的方法,它不修改输入的第一个参数,其将返回一个新的对象
1. i刚开始指向数据[1,2,3],完成后返回一个新对象,也就是[1, 2, 3, 1, 2, 3],不修改a,然后将该数据赋值给i
2. 第二次循环的时候,i又指向[4, 5, 6],返回一个新对象,也就是[4, 5, 6, 4, 5, 6],不修改B,将该数据赋值给i
3. 因此a,b都没有变,i = [4, 5, 6, 4, 5, 6]
"""
i = i*2
print("i=", i)
print("a=", a)
print("b=", b)
输出结果:
i= [4, 5, 6, 4, 5, 6]
a= [1, 2, 3]
b= [4, 5, 6]
# 可以打印结果出来看
a = [1, 2, 3]
b = [4, 5, 6]
for i in [a, b]:
i = i * 2
print(i)
输出:
[1, 2, 3, 1, 2, 3]
[4, 5, 6, 4, 5, 6]
而另外一种*=
a = [1, 2, 3]
b = [4, 5, 6]
for i in [a, b]:
i *= 2 # 它会修改a,b的值
print("i=", i)
print("a=", a)
print("b=", b)
输出结果:
i= [4, 5, 6, 4, 5, 6]
a= [1, 2, 3, 1, 2, 3]
b= [4, 5, 6, 4, 5, 6]
# 打印结果来看
a = [1, 2, 3]
b = [4, 5, 6]
for i in [a, b]:
i *= 2
print("i=", i)
print("a=", a)
print("b=", b)
print("----------")