65.9K
CodeProject 正在变化。 阅读更多。
Home

Python 3 中的浅拷贝与深拷贝

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2019年11月24日

CPOL

4分钟阅读

viewsIcon

10832

downloadIcon

263

本文使用 Python 3 介绍了浅拷贝与深拷贝之间的区别。

引言

在本文中,我们将讨论 Python 中浅拷贝和深拷贝的区别。

简单复制

让我们看一个例子

lst_first  = ['Yesterday', ['we', 'used'], 'the', ['Python', '2']] 
lst_second = lst_first
lst_third  = lst_first
lst_fourth = lst_first
          
print("\nOriginal list and three copies of it:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 
                    
print("\nLet's change the first word to “Today” and add the word “also” 
       ONLY at the end of the 4th list...")
lst_fourth [0] = 'Today' # replace the word 'Yesterday' with 'Today' 
lst_fourth.append('too') # add the word 'too'
          
print("\nLet's see the contents of the lists again:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth)

让我们看看上面代码的输出

Simple copying

哇....我们只更改了‘lst_fourth’列表中的值,但所有列表中的值都发生了改变。
为什么?!

在脚本的第一行,我们创建了lst_first列表,然后初始化了其他三个列表。
lst_first赋值给其他三个列表后,所有四个列表都引用相同的内存区域。
之后对其中任何一个内容的修改都会立即反映到所有其他列表的内容上。

让我们使用 Python 的内置函数id()来获取每个列表对象的“身份”(唯一整数)。

print()
print("Let's show “identities” (the addresses) of the objects in memory:")
print("id(lst_first)  = ",  id(lst_first))
print("id(lst_second) = ",  id(lst_second))
print("id(lst_third)  = ",  id(lst_third))
print("id(lst_fourth) = ",  id(lst_fourth))

上面示例的结果将是这样的:

Simple copying

我们可以看到,id()函数对于不同对象返回的值是相同的!
这证实了上面的说法,即所有四个列表对象都引用相同的地址空间!
从图上看,它看起来是这样的:

Simple copying

很明显,要真正复制列表,我们需要采用不同的方法。
让我们尝试另一种方法来创建独立的列表……

浅拷贝

下面的示例说明了创建浅拷贝的 3 种不同方法:

import copy

lst_first  = ['Yesterday', ['we', 'used'], 'the', ['Python', '2']] 
lst_second = copy.copy(lst_first) # make a shallow copy by using copy module 
lst_third  = list(lst_first)  # make a shallow copy by using the factory function
lst_fourth = lst_first[:] # make a shallow copy by using the slice operator 
          
print("\nOriginal list and three copies of it:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 
                    
print()
print("Let's show “identities” (the addresses) of the objects in memory:")
print("id(lst_first)  = ",  id(lst_first))
print("id(lst_second) = ",  id(lst_second))
print("id(lst_third)  = ",  id(lst_third))
print("id(lst_fourth) = ",  id(lst_fourth))

现在让我们看一下程序的运行结果:

Simple copying

好的。正如我们所看到的,所有列表(lst_secondlst_thirdlst_fourth)存储的值与原始列表(lst_first)相同。
然而,每个列表的id()函数返回的值显示的是不同的值。
这意味着这次,每个列表对象都有自己独立的地址空间。

注意

  1. 要复制到lst_second,‘copy’模块的‘copy’函数提供了通用的浅拷贝操作。
  2. list()接受lst_first对象的序列并将其转换为lst_third列表。list()构造函数返回一个可变序列列表的元素。
  3. 可以通过分配整个列表的切片来创建列表的浅拷贝,例如,lst_fourth = lst_first[:]。这里,lst_first列表返回一个包含所请求元素的新列表。这意味着上述切片运算符返回列表的副本,其中所有元素都将被复制到lst_fourth列表(即,到内存中的另一个区域)。

太棒了!
现在一切都如我们预期的那样工作了。所有三种方法都创建了独立的地址空间。
现在让我们修改上述列表的元素,为创建的列表添加更具体的信息:

lst_first [0] = 'In 2014'
lst_first.append(".7.9")

lst_second[0] = 'In 2015'
lst_second.append(".7.10")

lst_third [0] = 'In 2016'
lst_third.append(".7.13")

lst_fourth [0] = 'In 2017'
lst_fourth.append(".7.14")

print("\nLists after modification:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 

并运行代码:

Shallow copying

嗯,效果非常好,不是吗?
正如预期的那样,每个列表都包含其自身的 Python 发布使用历史数据。

但目前,让我们通知用户,新版本 Python 3.8 已于 2019 年 10 月 14 日发布。
为此,我们将仅修改其中一个列表的内容。

lst_fourth [0] = 'From October 14th, 2019'
lst_fourth [1][1] = 'are using'
lst_fourth [2] = 'a new release is'
lst_fourth [3][1] = '3.8'
lst_fourth.remove('.7.14') #remove the version extension, because we don't need it anymore 

print("\nLet's see ALL the lists again, after modifying the 'lst_fourth' list:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 

现在让我们再次运行脚本,看看最后 4 行打印的内容:

Shallow copying

糟糕……这完全不是我们期望看到的!
我们期望只在‘lst_fourth’中看到更改,但所有列表都发生了变化!
为什么所有列表都添加了值?

lst_first’列表包含三个元素和两个指向其他(内部)列表的指针,它们看起来像这样:['we', 'used']['Python', '2']。当我们对lst_first进行浅拷贝以创建列表时,只有简单的string元素被复制,但不是复合对象(我们的列表)。浅拷贝构造了一个新列表,然后将简单的字符串对象和指向原始对象中找到的复合对象的引用插入其中。因此,所有这些引用都指向同一个地址空间(即,指针被克隆,而不是它们指向的地址空间)。结果,‘lst_first’、‘lst_second’、‘lst_third’和‘lst_fourth’列表存储在单独的地址空间中,但同时,指向内部列表的指针指向一个地址。每当有人修改“他们的”内部列表时,问题就会出现,因为这种修改会影响所有内部列表的内容。

从图上看,它看起来是这样的:

Shallow copy

深拷贝

显然,在创建对象克隆时,我们需要采用不同的方法来强制 Python 完全复制列表中包含的所有列表元素(简单和复合对象)及其子列表或对象?
解决复制所有列表项问题的方法是进行深拷贝!
也就是说,我们不仅要复制复合对象的地址,还要强制 Python 完全复制列表中的所有列表元素(简单和复合对象)。这样,每个对象都有自己的地址空间,而不是引用一个公共对象。
让我们看看如何使用deepcopy()函数进行深拷贝:

import copy

lst_first  = ['Yesterday', ['we', 'used'], 'the', ['Python', '2']] 
lst_second = copy.deepcopy(lst_first)  
lst_third  = copy.deepcopy(lst_first)  
lst_fourth = copy.deepcopy(lst_first)   
          
print("\nOriginal list and three copies of it:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 
                    
print()
print("Let's show “identities” (the addresses) of the objects in memory:")
print("id(lst_first)  = ",  id(lst_first))
print("id(lst_second) = ",  id(lst_second))
print("id(lst_third)  = ",  id(lst_third))
print("id(lst_fourth) = ",  id(lst_fourth))

lst_fourth [0] = 'From October 14th, 2019'
lst_fourth [1][1] = 'are using'
lst_fourth [2] = 'a new release is'
lst_fourth [3][1] = '3.8'
lst_fourth.remove('.7.14') #remove the version extension, because we don't need it anymore 

print("\nLet's see ALL the lists again, after modifying the 'lst_fourth' list:")
print("lst_first  =", lst_first) 
print("lst_second =", lst_second) 
print("lst_third  =", lst_third) 
print("lst_fourth =", lst_fourth) 

让我们看看上面代码的输出:

Deep copying

从图上看,它看起来是这样的:

Simple copying

摘要

总结一下,我将提供 Python 文档 此处 的内容:

“浅拷贝和深拷贝的区别仅对于复合对象(包含其他对象的对象,如列表或类实例)才相关。

  • 浅拷贝构造了一个新的复合对象,然后(在可能的情况下)将原始对象中的引用插入其中。
  • 深拷贝构造了一个新的复合对象,然后递归地将原始对象中的对象的副本插入其中。”

历史

  • 2019年11月23日:初始版本
© . All rights reserved.