clone是深拷贝还是浅拷贝_Python中的深拷贝和浅拷贝以及存在的问题

v2-0709901845e300c03c1517ad1acf6aa5_1440w.jpg?source=172ae18b

在讨论浅拷贝和深拷贝之前,首先要了解python中两个对象比较==和is

==操作符进行的是对象的值判断,比较两个对象的值是否相等。 is操作符进行的是对象的身份标识的判断,比较两个对象的内存地址是否相等。

None在Python中比较特殊,在Python里是个单例对象,一个变量如果是None,它一定和None指向同一个内存地址。None是python中的一个特殊的常量,表示一个空的对象,空值是python中的一个特殊值。数据为空并不代表是空对象,例如[],''等都不是None。None和任何对象比较返回值都是False,除了自己。

列举几个例子

a = 1
b = 1

a == b
True

id(a) == id(b)
True

a is b
True

----------

a = 369
b = 369

a == b
True

id(a) == id(b)
False


a is b
False

首先 Python 会为 1这个值开辟一块内存,然后变量 a 和 b 同时指向这块内存地址,即 a 和 b 都是指向 1 这个变量,因此 a 和 b 的值相等,id 也相等,a == b和a is b都返回 True。

不过,需要注意,对于整型数字来说,以上a is b为 True 的结论,只适用于 [-5 , 257 )范围内的数字。

出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。每次创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的值的引用,而不是重新频繁地开辟一块新的内存空间。

上述例子中的 369,Python 则会为两个369开辟两块内存区域,因此 a 和 b 的 ID 不一样,a is b就会返回 False 了。

浅拷贝shallow copy

浅拷贝通常的实现方法是使用数据类型本身的构造器,此处所说通常情况,是因为后面存在着特殊情况(元组的构造器),比如:

l1 = [1, 2, 3]
l2 = list(l1) # l2 = l1[:]

l2
[1, 2, 3]

l1 == l2
True

l1 is l2
False

---------------

s1 = set([1, 2, 3])
s2 = set(s1)

s2
{1, 2, 3}

s1 == s2
True

s1 is s2
False

当然,Python 中也提供了相对应的函数 copy.copy(),适用于任何数据类型,用来进行浅拷贝:

import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)

前面提到了元组的特殊情况,使用 tuple() 或者切片操作符':'不会创建一份浅拷贝,相反,它会返回一个指向相同元组的引用。实际上,使用copy.copy()以及copy.deepcopy()得到的,也是一个指向相同元组的引用。

t1 = (1, 2, 3)
t2 = tuple(t1)

t1 == t2
True

t1 is t2
True

浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。既然拷贝得到的新的对象的元素是对原对象中子对象的引用,那么有几个点需要注意一下。看一下下面的例子。

# ----- 1 -----
l1 = [[1, 2], (30, 40)]
l2 = list(l1)  
# ----- 1 -----

# ----- 2 -----
l1.append(100) 

l1
[[1, 2, 3], (30, 40), 100]

l2
[[1, 2, 3], (30, 40)]
# ----- 2 -----

# ----- 3&4 -----
l1[0].append(3)
l1[1] += (50, 60)
l1
[[1, 2, 3], (30, 40, 50, 60), 100]

l2
[[1, 2, 3], (30, 40)]
  1. 这个例子中,我们首先初始化了一个列表 l1,里面的元素是一个列表和一个元组;然后对 l1 执行浅拷贝,赋予 l2。因为浅拷贝里的元素是对原对象元素的引用,因此 l2 中的元素和 l1 指向同一个列表和元组对象。
  2. 紧接着,l1.append(100),表示对 l1 的列表新增元素 100。这个操作不会对 l2 产生任何影响,因为 l2 和 l1 作为整体是两个不同的对象,并不共享内存地址。操作过后 l2 不变,l1 会发生改变。
  3. 再来看,l1[0].append(3),这里表示对 l1 中的第一个列表新增元素 3。因为 l2 是 l1 的浅拷贝,l2 中的第一个元素和 l1 中的第一个元素,共同指向同一个列表,因此 l2 中的第一个列表也会相对应的新增元素 3。操作后 l1 和 l2 都会改变。
  4. 最后是l1[1] += (50, 60),因为元组是不可变的,这里表示对 l1 中的第二个元组拼接,然后重新创建了一个新元组作为 l1 中的第二个元素,而 l2 中没有引用新元组,因此 l2 并不受影响。操作后 l2 不变,l1 发生改变。

上面的例子,对于可变对象的引用,尤其是列表独享,容易带来一些副作用,要避免这种情况,可以使用深拷贝。

深拷贝deep copy

Python 中以 copy.deepcopy() 来实现对象的深度拷贝。

import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)

l1
[[1, 2, 3], (30, 40), 100]

l2 
[[1, 2], (30, 40)]

可以看到,无论 l1 如何变化,l2 都不变。因为此时的 l1 和 l2 完全独立,没有任何联系。

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

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

x
[1, [...]]

y = copy.deepcopy(x)
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 # 如果字典里已经存储了将要拷贝的对象,则直接返回
        ...

如果执行如下操作:

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

y = copy.deepcopy(x)

# 重点来了
x == y
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-276-9cfbd892cdaa> in <module>
----> 1 x == y

RecursionError: maximum recursion depth exceeded in comparison

出现上面错误的原因很明显,在执行 == 操作时,因为x中存储了自身的引用,会无限的递归与y比较,从而造成RecursionError异常,因为最大递归深度有一定的限制。

如果执行len操作,求x的长度,你会发现

len(x) == 2
True

总结

  • 比较操作符'=='表示比较对象间的值是否相等,而'is'表示比较对象的标识是否相等,即它们是否指向同一个内存地址。
  • 浅拷贝中的元素,是原对象中子对象的引用,因此,如果原对象中的元素是可变的,改变其也会影响拷贝后的对象,存在一定的副作用。
  • 深度拷贝则会递归地拷贝原对象中的每一个子对象,因此拷贝后的对象和原对象互不相关。另外,深度拷贝中会维护一个字典,记录已经拷贝的对象及其 ID,来提高效率并防止无限递归的发生。

最后供上一张图方便理解:

v2-0709901845e300c03c1517ad1acf6aa5_b.jpg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值