python高级编程之03可变对象与不可变对象的浅拷贝与深拷贝以及`+=`与`+`的区别

前言

Python表达式中i += xi = 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]

为什么上下两段代码结果会不一样呢,弄清楚这个问题之前首先得明白两个概念:

  1. 可变对象
  2. 不可变对象

可变对象和不可变对象

在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))

可变对象的浅拷贝

  1. =浅拷贝——值相等,地址相等
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]
  1. 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的区别

  1. 既然都是值相等,地址不等,那么和copy浅拷贝有什么区别呢?

对可变对象而言,对象的值一样可能包含有对其他对象的引用。浅拷贝产生的新对象,虽然具有完全不同的id,但是其值若包含可变对象,这些对象和原始对象中的值包含同样的引用,一旦对其进行修改,那么相应的也会发生修改

  1. 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不同,但是引用对象是可变对象时候,一旦引用对象发生了变化,其也会跟着发生变化
  1. 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进行了修改']
  2. 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("----------")

猜你喜欢

转载自blog.csdn.net/weixin_40920290/article/details/81147862