专栏首页Python与算法之美图解 Python 浅拷贝与深拷贝

图解 Python 浅拷贝与深拷贝

Python 中的赋值语句不会创建对象的拷贝,仅仅只是将名称绑定至一个对象。对于不可变对象,通常没什么差别,但是处理可变对象或可变对象的集合时,你可能需要创建这些对象的 “真实拷贝”,也就是在修改创建的拷贝时不改变原始的对象。

本文将以图文方式介绍 Python 中复制或“克隆”对象的操作。

首先介绍一下 Python 中浅拷贝与深拷贝的区别:

  • 浅拷贝:浅拷贝意味着构造一个新的集合对象,然后用原始对象中找到的子对象的引用来填充它。从本质上讲,浅层的复制只有一层的深度。复制过程不会递归,因此不会创建子对象本身的副本。
  • 深拷贝:深拷贝使复制过程递归。这意味着首先构造一个新的集合对象,然后递归地用在原始对象中找到的子对象的副本填充它。以这种方式复制一个对象,遍历整个对象树,以创建原始对象及其所有子对象的完全独立的克隆。

赋值与引用

在开始浅拷贝与深拷贝前,我们先来看一下 Python 中的赋值与引用。

lst = [1, 2, 3]
new_list = lst

从字面上看,上述语句创建了变量 lstnew_list,并且 lstnew_list 的赋值都为一个列表。但是,Python 的赋值语句并不会复制对象,而是会重新创建一个对象的引用。

可以看出,lstnew_list 都引用了同一个列表。

创建浅拷贝

不少教程里都会提到,如果你有一个列表,当你想要修改列表中的值但却不想影响原始对象时,可以使用 list 复制(浅拷贝)一个列表。

我们先来试一下:

lst = [1, 2, 3]
new_list = list(lst)

没错,lstnew_list 分别指向了不同的列表。当修改 lst 列表中的值时,并不会对 new_list 对象产生影响。

lst[0] = 'x'
print(lst)
print(new_list)
['x', 2, 3]
[1, 2, 3]

之所以说 list 语句是浅拷贝,是因为这种修改只对一层对象有效,当列表中有子对象时,对子对象的修改将影响原始对象和浅拷贝对象。

为了解释这一说法,让我们先创建一个嵌套列表,并使用 list 函数创建浅拷贝。

lst = [[1, 2, 3], [4, 5, 6]]
new_list = list(lst)

这里 new_list 是有着和 lst 一样内容的新的独立的对象。

可以看到 lstnew_list 分别指向了不同的对象。

对第一层 lst 的修改,将不会对 new_list 副本造成影响。

lst.append([7, 8, 9])
print(lst)
print(new_list)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[[1, 2, 3], [4, 5, 6]]

但是,因为我们只创建了原始列表的一个浅拷贝,所以 new_list 仍然包含对 lst 中存储的原始子对象的引用。

也就是如上图所示,lstnew_list 的子列表都指向了相同的对象。

子对象没有被复制,它们只是在复制的列表中被再次引用。

因此,当你修改 lst 中的一个子对象时,这种修改也会反映到 new_list 中—— 这是因为两个列表共享相同的子对象。这种复制只是一个浅的,一个层级的复制:

lst[0][0] = 'x'
print(lst)
print(new_list)
[['x', 2, 3], [4, 5, 6], [7, 8, 9]]
[['x', 2, 3], [4, 5, 6]]

如果我们在第一步中创建了一个 lst 的深拷贝,那么两个对象就完全独立了。这是对象的浅拷贝和深拷贝之间的实际区别。

使用 Python 标准库中的 copy 模块可以创建深拷贝,这个模块为创建任意 Python 对象的浅拷贝和深拷贝提供了一个简单的接口。

创建深拷贝

这次我们使用 deepcopy() 函数创建一个对象的深拷贝:

import copy
lst = [[1, 2, 3], [4, 5, 6]]
new_list = copy.deepcopy(lst)

从图中可以看出 lstnew_list 中的子对象指向了不同的对象,如果对 lst 的子对象进行修改,将不会影响 new_list

这一次,原始对象和复制对象都是完全独立的。如前面所说,递归克隆了 lst,包括它的所有子对象:

lst[0][0] = 'x'
print(lst)
print(new_list)
[['x', 2, 3], [4, 5, 6]]
[[1, 2, 3], [4, 5, 6]]

copy 模块中的 copy.copy() 函数也可以创建对象的浅拷贝。使用 copy.copy() 可以明确地表示创建浅拷贝。对于内置集合,简单地使用 listdictset 等工厂函数来创建浅拷贝是更加 Pythonic 的。

复制任意 Python 对象

copy.copy()copy.deepcopy() 函数可用于复制任意对象。以前面的列表复制示例为基础。让我们从定义一个简单的 2D 点类开始:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point({self.x!r}, {self.y!r})'

__repr__() 函数使我们可以轻松地在 Python 解释器中检查从这个类创建的对象。

接下来,我们将创建一个 Point 实例,然后使用 copy 模块复制(浅拷贝)它:

a = Point(23, 42)
b = copy.copy(a)
print(a is b)
False

ab 分别指向了不同的 Point 实例。因为我们的 Point 对象使用不可变类型(int)作为其坐标,所以在这种情况下,浅拷贝和深拷贝没有区别。但我马上会展开这个例子。

接下来定义另一个类来表示 2D 矩形。矩形将使用 Point 对象来表示它们的坐标:

class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def _repr__(self):
        return (f'Rectangle({self.topleft!r}, {self.bottomright!r})')

# 创建一个 Rectangle 实例的浅拷贝
rect = Rectangle(Point(0, 1), Point(5, 6))
shallow_rect = copy.copy(rect)

print(rect)
print(shallow_rect)
print(rect is shallow_rect)
Rectangle(Point(0, 1), Point(5, 6))
Rectangle(Point(0, 1), Point(5, 6))
False

跟前面 list 的例子一样,rectshallow_rect 的子对象都有相同的引用。在对象层级中修改一个对象,将看到这个变化也反映在浅拷贝的副本中:

rect.topleft.x = 999
print(rect)
print(shallow_rect)
Rectangle(Point(999, 1), Point(5, 6))
Rectangle(Point(999, 1), Point(5, 6))

接下来创建 Rectangle 的深拷贝并对其进行修改:

deep_rect = copy.deepcopy(rect)
deep_rect.topleft.x = 222
print(rect)
print(shallow_rect)
print(deep_rect)
Rectangle(Point(999, 1), Point(5, 6))
Rectangle(Point(999, 1), Point(5, 6))
Rectangle(Point(222, 1), Point(5, 6))

可以看出,深拷贝完全独立于原始对象和浅拷贝对象。

参阅 copy 模块文档 可以对复制进行进一步的研究。例如,对象可以通过定义特殊的方法 __copy__()__deepcopy__() 来控制如何复制它们。

谨记三件事

  • 创建对象的浅拷贝不会克隆子对象。因此,拷贝不会完全独立于原始对象。
  • 一个对象的深拷贝会递归地克隆子对象。克隆对象完全独立于原始对象,但是创建深拷贝速度较慢。
  • 可以使用 copy 模块复制任意对象(包括自定义类)。

本文分享自微信公众号 - Python与算法之美(Python_Ai_Road)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 特征列feature_column

    特征列 通常用于对结构化数据实施特征工程时候使用,图像或者文本数据一般不会用到特征列。

    lyhue1991
  • 损失函数losses

    一般来说,监督学习的目标函数由损失函数和正则化项组成。(Objective = Loss + Regularization)

    lyhue1991
  • 三种计算图

    在TensorFlow1.0时代,采用的是静态计算图,需要先使用TensorFlow的各种算子创建计算图,然后再开启一个会话Session,显式执行计算图。

    lyhue1991
  • Python中的具名元组类用法

    >>> from collections import namedtuple >>> Point = namedtuple('Point', ['x', 'y'...

    Python小屋屋主
  • C++构造/析构函数

    当类的成员变量中存在类时候,同时成员类没有无参或默认构造函数,在创建该类的对象时候会出错。这是需要使用初始化列表。将需要的成员变量进行初始化。

    用户2929716
  • 平面上给定n条线段,找出一个点,使这个点到这n条线段的距离和最小。

    题目:平面上给定n条线段,找出一个点,使这个点到这n条线段的距离和最小。 源码如下: 1 #include <iostream> 2 #include ...

    Angel_Kitty
  • 吴恩达机器学习笔记-1

    这个系列教程大名鼎鼎,之前我都是用到啥就瞎试一通;最近花了两个周,认认真真把这些基础知识重新学了一遍;做个笔记; 苏老泉二十七始发愤,我这比他还落后;不过求知的...

    happy123.me
  • 「HDU - 2857」Mirror and Light(点关于直线的对称点)

    一条直线代表镜子,一个入射光线上的点,一个反射光线上的点,求反射点。(都在一个二维平面内)

    饶文津
  • Windows 安装和配置 WSL

    我们简单的认为它是在 Windows 上安装了一个 Linux 环境就好了。也就是最好的 Linux 发行版:Win10 + WSL (滑稽)。

    希希里之海
  • Ruby练习一=> {'a' => 3, 'man' => 1, 'canal' => 1, 'panama' => 1, 'plan' => 1}returns the list ["Pam", "

    用户2183996

扫码关注云+社区

领取腾讯云代金券