Python的浅拷贝和深拷贝

Python浅拷贝和深拷贝

浅拷贝(shallow copy)和深度拷贝(deep copy)

1.===is[:]的区别

  • =是赋值操作符,将左侧变量指向右侧变量的内存地址
  • ==只能用于判断两个变量的值是否相等,不能用于赋值,也不比较内存地址
  • is判断两种是否指向同一个内存地址
  • [:]是切片操作符,用于复制列表、元组、字符串等序列类型,返回一个浅拷贝的副本

2.浅拷贝和深拷贝的区别

  • 浅拷贝:指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。
  • 深拷贝:所谓深度拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。

例一:

a =  [1, 2, [3, 4]]

b = a #让b指向a指向的地方,也就是a和b是同一个东西
print(a is b) #True
print(a == b) #True

c = list(a) #浅拷贝
print(a is c) #False
print(a == c) #True

d = a[:] #浅拷贝
print(a is d) #False
print(a == d) #True

#但是对于不可改变的数据类型
m = (1, 2, [3, 4])
e = tuple(m)
print(m is e) #True
print(m == e) #True

到这里,对于浅拷贝你应该很清楚了。浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。因此,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会带来一些副作用,尤其需要注意。我们来看下面的例子:

例二:

a = [[1, 2], (30, 40)]
b = list(a)

a.append(100) #这个操作不会对 b 产生影响,因为 a 和 b 作为整体是两个不同的对象,并不共享内存地址
a[0].append(3)#因为 b 是 a 的浅拷贝,b 中的第一个元素和 a 中的第一个元素,共同指向同一个列表,因此 b 中的第一个列表也会相对应的新增元素 3。

print(a) #[[1, 2, 3], (30, 40), 100]
print(b) #[[1, 2, 3], (30, 40)]
#这里就很清晰了,对于拷贝列表里面的元素,是共享一个内存地址,但是列表整体不是一个内存地址
#浅拷贝里的元素是对原对象元素的引用,因此 b 中的元素和 a 指向同一个列表和元组对象

a[1] += (50, 60) #因为元组是不可变的,这里表示对 a 中的元组拼接,然后重新创建了一个新元组作为 a 中的第二个元素,而 b 中没有引用新元组,因此 b 并不受影响。

print(a) #[[1, 2, 3], (30, 40, 50, 60),100]
print(b) #[[1, 2, 3], (30, 40)]

通过这个例子,你可以很清楚地看到使用浅拷贝可能带来的副作用。因此,如果我们想避免这种副作用,完整地拷贝一个对象,你就得使用深度拷贝。

例三:copy和deepcopy函数

import copy
a = [[1, 2], (30, 40)]

b = copy.copy(a)
print(a is b) #False
print(a == b) #True

#Python 中以 copy.deepcopy() 来实现对象的深度拷贝
c = copy.deepcopy(a) #False
print(a is c) #True

a.append(100)
print(b) #[[1, 2], (30, 40)]
print(c) #[[1, 2], (30, 40)]

a[0].append(3)
print(b) #[[1, 2, 3], (30, 40)]
print(c) #[[1, 2], (30, 40)]

可以看到无论 a 怎么变化,c 始终不受影响,因为它们完全独立,没有联系。

不过,深度拷贝也不是完美的,往往也会带来一系列问题。如果被拷贝对象中存在指向自身的引用,那么程序很容易陷入无限循环:

import copy
x = [1]
x.append(x)

y = copy.deepcopy(x)
print(x) #[1, [...]]
print(y) #[1, [...]]

上面这个例子,列表 x 中有指向自身的引用,因此 x 是一个无限嵌套的列表。但是我们发现深度拷贝 x 到 y 后,程序并没有出现 stack overflow 的现象。这是为什么呢?

其实,这是因为深度拷贝函数 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,我们来看相对应的源码就能明白:

def deepcopy(x, memo=None, _nil=[]):
    """Deep copy operation on arbitrary Python objects.
    	
	See the module's __doc__ string for more info.
	"""
	
    if memo is None:
        memo = {}
    d = id(x) # 查询被拷贝对象 x 的 id
	y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
	if y is not _nil:
	    return y # 如果字典里已经存储了将要拷贝的对象,则直接返回
        ...    

一些需要注意的地方

通常来说,在实际工作中,当我们比较变量时,使用’==‘的次数会比’is’多得多,因为我们一般更关心两个变量的值,而不是它们内部的存储地址。但是,当我们比较一个变量与一个单例(singleton)时,通常会使用’is’。一个典型的例子,就是检查一个变量是否为 None:

if a is None:
      ...
 
if a is not None:
      ...

这里注意,比较操作符is的速度效率,通常要优于==。因为is操作符不能被重载,这样,Python 就不需要去寻找,程序中是否有其他地方重载了比较操作符,并去调用。执行比较操作符is,就仅仅是比较两个变量的 ID 而已。

但是==操作符却不同,执行a == b相当于是去执行a.__eq__(b),而 Python 大部分的数据类型都会去重载__eq__这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__函数会去遍历列表中的元素,比较它们的顺序和值是否相等。

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值