学习Python中的浅层和深层拷贝以及如何使用__copy__()

149 阅读15分钟

你需要在Python程序中对一个对象进行复制。这能有多难呢?不是很困难。但是你还需要知道Python中浅拷贝和深拷贝的区别,并决定你需要哪一个。

在这篇文章中,你将读到在简单数据结构上使用时浅拷贝和深拷贝的区别。然后,你会看到更复杂的结构,包括复制一个由你自己定义的类所创建的对象时。在这个例子中,我将克隆我自己(!),你将看到复制对象的一些陷阱以及如何注意和避免它们。

在这篇文章中,你会学到更多的东西。

  • 创建简单列表和其他数据结构的副本
  • 创建更复杂的列表的副本
  • 使用**copy** 内置模块
  • 了解Python 中浅层拷贝和深层拷贝之间的区别
  • **使用__copy__()**来定义如何浅层拷贝一个用户定义的类的对象

是的,还有__deepcopy__() ,但在这篇文章中我将止步于__copy__()

复制对象的问题是什么?

下面是你在本文最后要写的例子的预览。你将创建几个简单的类来定义一个Person 和一个Car 。是的,恐怕又是 "人 "和 "车"。你已经在面向对象的编程教程中经常看到这些例子。但在这种情况下有点不同,所以请你忍耐一下。

如果你想要一个关于类的教程,而不是像其他所有教程那样使用 "老一套的类",你可以阅读 The Python Coding Book 中关于Python 中的面向对象编程的章节。

# household.py

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.mileage = 0

    def add_mileage(self, miles: float):
        self.mileage += miles

class Person:
    def __init__(self, firstname: str):
        self.firstname = firstname
        self.car = None

    def buy_car(self, car: Car):
        self.car = car

    def drive(self, miles: float):
        self.car.add_mileage(miles)

稍后你会更详细地了解这个例子。现在,在这里,我将强调Car 模块有make,model, 和mileage 属性。后者可以使用add_mileage() 方法进行更新。

Person 有属性 和 。你可以使用 ,将一个类型为 的对象分配给 ,你可以使用 ,让这个人去开车,这样可以增加汽车的里程。firstname car buy_car() Car Person drive()

你可以在一个新的脚本中使用这些类。

# cloning_stephen.py

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

print() 行的输出是。

Stephen's mileage is 100 miles

接下来,你将克隆斯蒂芬(好像一个我还不够!)。

# cloning_stephen.py

import copy

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

# Let's copy the Person instance
clone = copy.copy(stephen)

print(
    f"The clone's car is a {clone.car.make} {clone.car.model}"
)

print(f"The clone's mileage is {clone.car.mileage} miles")

# Let's check whether the two cars are exactly the same car
print(
    f"Stephen's car is clone's car: {stephen.car is clone.car}"
)

而问题就出在这里。看看这段代码的输出。

Stephen's mileage is 100 miles
The clone's car is a BMW Series 1
The clone's mileage is 100 miles
Stephen's car is clone's car: True

克隆人的车也是一辆宝马1系列,这很合理。克隆人的品味和需求与斯蒂芬一样!但是,克隆人的车也是宝马1系,这很合理。但是,克隆人的车从100英里开始。尽管你刚刚创建了这个克隆人,而且他还没有开过车。

最后一句话解释了正在发生的事情。斯蒂芬和克隆人有同样的车。不只是相同的品牌和型号,而是完全相同的车。

如果克隆人现在去开车,斯蒂芬的里程数也会改变。如果你在cloning_stephen.py 的末尾加上以下几行,会发生什么?

# cloning_stephen.py

# ...

# Clone goes for a drive:
clone.drive(68)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

输出结果是。

Stephen's mileage is 168 miles

史蒂芬的里程数增加了68英里,尽管是克隆人去开车了。这是因为他们使用的是同一辆车!当你创建一个Person 的副本时,这不可能是你想要的行为。

稍后你将回到这个例子。

制作简单数据结构的拷贝

我将快速浏览这一节,因为有趣的内容在下一节开始。让我们来复制一个列表和一个字典。

>>> trip_mileages = [10, 12, 3, 59]
>>> copied_list = trip_mileages.copy()
>>> copied_list
[10, 12, 3, 59]
>>> copied_list is trip_mileages
False

>>> trips = {
...     "Supermarket": 2,
...     "Holiday": 129,
... }
>>> copied_dict = trips.copy()
>>> copied_dict
{'Supermarket': 2, 'Holiday': 129}
>>> copied_dict is trips
False

列表字典都有一个.copy() 方法。这使得复制它们来创建一个包含相同信息的新对象变得容易。

如果你有一个元组呢?

>>> trip_mileages_tuple = 10, 12, 3, 59
>>> trip_mileages_tuple.copy()
Traceback (most recent call last):
  ...
AttributeError: 'tuple' object has no attribute 'copy'

元组没有一个.copy() 方法。在这种情况下,你可以尝试使用copy 内置模块。

>>> trip_mileages_tuple = 10, 12, 3, 59

>>> import copy

>>> copied_tuple = copy.copy(trip_mileages_tuple)
>>> copied_tuple
(10, 12, 3, 59)
>>> copied_tuple is trip_mileages_tuple
True

你已经能够创建一个元组的 "副本 "了,只不过它根本不是一个副本!你可以用这个方法创建一个元组。由于元组是不可变的,当你试图复制元组时,你会得到一个对同一元组的新引用。

你可能想知道,如果你将copy.copy() 与可变类型(如列表和字典)一起使用,是否也是这种情况。

>>> trip_mileages = [10, 12, 3, 59]

>>> import copy

>>> copied_list = copy.copy(trip_mileages)
>>> copied_list
[10, 12, 3, 59]
>>> copied_list is trip_mileages
False

不,在这种情况下,copy.copy(trip_mileages) 的输出与trip_mileages.copy() 相同。你将在后面看到是什么决定了copy.copy() 在任何对象上的行为。但首先,让我们看看更复杂的数据结构,了解浅层和深层拷贝。

制作复杂数据结构的副本

考虑一个团队的列表,其中每个团队是一个名字的列表。你创建一个团队列表的副本。

>>> teams = [["Stephen", "Mary"], ["Kate", "Trevor"]]
>>> copied_teams = teams.copy()
>>> copied_teams
[['Stephen', 'Mary'], ['Kate', 'Trevor']]
>>> copied_teams is teams
False

到目前为止,这与上一节中的结果相同。但是,马丁加入了斯蒂芬和玛丽的团队。你选择将其添加到复制的列表中,因为你想保持原始teams 列表不变。

>>> copied_teams[0].append("Martin")
>>> copied_teams
[['Stephen', 'Mary', 'Martin'], ['Kate', 'Trevor']]

>>> teams
[['Stephen', 'Mary', 'Martin'], ['Kate', 'Trevor']]

>>> copied_teams[0] is teams[0]
True

你将马丁加入到copied_teams 中的第一个团队中。然而,他也被加入到teams 中的第一个团队中,即原始列表中,尽管你没有明确地附加任何东西到其中。

你可以在最后一条语句中看到为什么会发生这种情况,你要检查copied_teams 中的第一个列表与teams 中的第一个列表是否是同一个对象。是的,它们都是同一个对象。

在 Python 中创建浅层和深层拷贝

当你使用teams.copy() 复制列表时,你创建了一个列表的浅层拷贝。让我们看看这意味着什么。

当你创建一个 list 时,你是在创建一个包含若干项的 list 类型的新对象。然而,这个列表实际上包含了对其他对象的引用,这些对象被保存在其他地方。因此,teams[0] 是对另一个对象即 list 的引用。['Stephen', 'Mary'].再看一下你最初用来创建teams 列表的那一行。

>>> teams = [["Stephen", "Mary"], ["Kate", "Trevor"]]

这一行创建了三个列表。

  • 列表['Stephen', 'Mary']
  • 列表['Kate', 'Trevor']
  • 名为teams 的列表,它引用了其他两个列表。

你可以用下图来形象地说明这一点。 image.png 当你使用teams.copy()copy.copy(teams) 时,你正在创建一个新的外部列表。然而,你并没有复制内部列表。相反,你使用了你已经拥有的相同的列表['Stephen', 'Mary']['Kate', 'Trevor'] 。下面是这种情况的描述。 image.png teams[0] 和 是指向同一个列表的两个引用。你有两种方式来引用同一个对象。copied_teams[0]

所以,当你把Martin添加到copied_teams[0] ,你是把Martin的名字添加到唯一的现有列表中,这个列表中有Stephen团队成员的名字。

有时,这不是你想要的。相反,你想在对象内部创建一个所有项目的副本。

深度复制

在本节中,你将读到关于创建一个对象的深度拷贝。但首先,让我们使用内置模块copy 中的函数重新创建上面的例子。

copy.copy() 创建一个浅层拷贝,所以你会得到和上节一样的输出。

>>> import copy
>>> teams = [["Stephen", "Mary"], ["Kate", "Trevor"]]

>>> copied_teams = copy.copy(teams)

>>> copied_teams[0].append("Martin")
>>> copied_teams
[['Stephen', 'Mary', 'Martin'], ['Kate', 'Trevor']]
>>> teams
[['Stephen', 'Mary', 'Martin'], ['Kate', 'Trevor']]

>>> copied_teams[0] is teams[0]
True

因此对于列表来说,copy.copy(teams)teams.copy().

接下来,你可以尝试使用 copy.deepcopy()来代替。

>>> import copy
>>> teams = [["Stephen", "Mary"], ["Kate", "Trevor"]]

>>> deepcopied_teams = copy.deepcopy(teams)
>>> deepcopied_teams
[['Stephen', 'Mary'], ['Kate', 'Trevor']]

>>> deepcopied_teams[0].append("Martin")
>>> deepcopied_teams
[['Stephen', 'Mary', 'Martin'], ['Kate', 'Trevor']]
>>> teams
[['Stephen', 'Mary'], ['Kate', 'Trevor']]

>>> deepcopied_teams[0] is teams[0]
False

当你将"Martin" 附加到deepcopied_teams 上时,也就是你从原始列表中创建的深度拷贝,当你显示teams 时,新的项目不会出现。而且与前面浅层拷贝的情况不同,deepcopied_teams[0]teams[0] 不再是同一个对象。

当你创建一个深度拷贝时,你是在复制外层列表,但你也在创建内部列表的拷贝。因此,teams 中的引用和deepcopied_teams 中的引用指向了不同的对象。由deepcopy() 创建的两个副本是完全独立的。下面是这个表示法现在的样子。

image.png

复制你自己定义的类的对象

现在是时候创建你自己的类并探索当你复制它们时会发生什么。在本文的开头,你已经看到了类的定义CarPerson 。让我们好好介绍一下这些类。你可以在一个叫做household.py 的脚本中定义它们。

# household.py

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.mileage = 0

    def add_mileage(self, miles: float):
        self.mileage += miles

class Person:
    def __init__(self, firstname: str):
        self.firstname = firstname
        self.car = None

    def buy_car(self, car: Car):
        self.car = car

    def drive(self, miles: float):
        self.car.add_mileage(miles)

你可以用一个make 和一个model 来初始化Car ,这两个都是字符串。在这个例子中,我使用了类型提示来跟踪参数类型是什么。一辆新车开始时,mileage 为0英里(或者公里,如果你愿意的话)。

顾名思义,add_mileage() 方法是用来在人驾驶汽车时增加里程。

一个Person 被初始化为一个名字,这个名字是一个字符串。方法buy_car() 允许你将类Car 的一个实例链接到Person 的一个实例上。Car 对象是用属性Person.car 来引用的。

每当这个人去旅行时,你可以调用drive() 方法,该方法将额外的里程记录在这个人的车上。

在一个名为cloning_stephen.py 的新脚本中,你可以测试这些类。

# cloning_stephen.py

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

这与你之前看到的代码相同。你创建一个Person 的实例,并为该实例调用buy_car() 方法。斯蒂芬(我还是用第三人称谈论自己!)去开了100英里的车。你通过调用drive() 方法来记录此事。这将更新stephen.car 中引用的Car 实例的mileage 属性。这段代码给出了以下输出。

Stephen's mileage is 100 miles

复制一个对象。默认案例

斯蒂芬最近很忙!他决定克隆自己,这样他就可以获得更多的信息。他决定克隆自己,这样他就可以完成更多的事情了。让我们来试试这个。你可以使用内置的copy.copy() ,复制cloning_stephen.py 中的实例stephen

# cloning_stephen.py

import copy

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

# Let's copy the Person instance
clone = copy.copy(stephen)

print(
    f"The clone's car is a {clone.car.make} {clone.car.model}"
)

print(f"The clone's mileage is {clone.car.mileage} miles")

# Let's check whether the two cars are exactly the same car
print(
    f"Stephen's car is clone's car: {stephen.car is clone.car}"
)

这个脚本的输出结果,你已经在前面看到了,显示了这种类型的复制的问题。

Stephen's mileage is 100 miles
The clone's car is a BMW Series 1
The clone's mileage is 100 miles
Stephen's car is clone's car: True

这是一个浅层拷贝。因此,尽管stephenclone 是类Person 的不同实例,它们都共享同一个实例Car 。Stephen 已经成功地克隆了自己,但是他必须与他的克隆人共享同一辆车。这不是好事,因为如果斯蒂芬和克隆人不能去不同的地方,他们就不能提高效率。

如果克隆人去开车,他使用的是斯蒂芬使用的同一辆车。因此,额外的里程数也会显示在斯蒂芬身上。

# cloning_stephen.py

# ...

# Clone goes for a drive:
clone.drive(68)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

这表明斯蒂芬的里程数已经增加到168英里。

Stephen's mileage is 168 miles

使用copy.deepcopy()

如果你试图创建一个深度拷贝而不是浅度拷贝呢?毕竟,这个技巧在前面的团队成员列表的例子中是有效的。你可以更新cloning_stephen.py ,使用copy.deepcopy() ,而不是copy.copy()

# cloning_stephen.py

import copy

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

# Let's copy the Person instance
clone = copy.deepcopy(stephen)

print(
    f"The clone's car is a {clone.car.make} {clone.car.model}"
)

print(f"The clone's mileage is {clone.car.mileage} miles")

# Let's check whether the two cars are exactly the same car
print(
    f"Stephen's car is clone's car: {stephen.car is clone.car}"
)

当你运行这个脚本时,你现在会得到以下输出。

Stephen's mileage is 100 miles
The clone's car is a BMW Series 1
The clone's mileage is 100 miles
Stephen's car is clone's car: False

斯蒂芬的里程数仍然是100英里。没有理由会有不同,因为Stephen开了100英里。

克隆人的汽车是宝马1系列,与斯蒂芬的汽车品牌和型号相同。这就是你想要的,因为Stephen的克隆人与Stephen有相同的汽车偏好!

让我们跳到输出的最后一行。斯蒂芬的车不再是与克隆人的车完全一样的车。这与上面的浅层复制得到的结果不同。克隆人的车是Car 的不同实例。所以现在有两辆车;一辆属于Stephen,另一辆属于克隆人。

然而,克隆人的车在里程表上已经有100英里,尽管克隆人还没有开车。当你创建一个stephen 的深度拷贝时,程序会创建一个新的Car 的实例。然而,所有原来的汽车属性也被复制了。这意味着,当你创建深度拷贝时,克隆人的汽车以Stephen的汽车的任何里程数开始。

从现在开始,这两辆车是分开的,所以当克隆人驾驶这辆车时,额外的里程数不会显示在斯蒂芬的车上。

# cloning_stephen.py

# ...

# Clone goes for a drive:
clone.drive(68)

print(f"Stephen's mileage is {stephen.car.mileage} miles")
print(f"The clone's mileage is {clone.car.mileage} miles")

输出结果显示,Stephen的里程数仍然是100英里,但克隆人的里程数现在是168英里,尽管他唯一的一次旅行是68英里长。

...
Stephen's mileage is 100 miles
The clone's mileage is 168 miles

在本文的最后一节,你将解决这个问题,自定义Person 的实例应该如何被复制。

定义__copy__ Dunder方法

你可以为你定义的任何类重写copy.copy()copy.deepcopy() 的默认行为。在这篇文章中,我将只专注于定义dunder方法 __copy__(),它决定了当你为你的对象调用copy.copy() 时会发生什么。还有一个__deepcopy__() dunder方法,旨在创建深度拷贝,它与此类似,但提供了更多的功能来处理复杂的对象。

你可以回到household.py ,在这里你定义了Person ,并将__copy__() 添加到该类。

# household.py

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.mileage = 0

    def add_mileage(self, miles: float):
        self.mileage += miles

class Person:
    def __init__(self, firstname: str):
        self.firstname = firstname
        self.car = None

    def buy_car(self, car: Car):
        self.car = car

    def drive(self, miles: float):
        self.car.add_mileage(miles)

    def __copy__(self):
        copy_instance = Person(self.firstname)
        copy_instance.buy_car(
            Car(
                make=self.car.make,
                model=self.car.model,
            )
        )
        return copy_instance

__copy__() dunder 方法创建一个新的Person 实例,使用与你要复制的实例相同的名字。它还使用你要复制的汽车的品牌和型号创建一个新的Car 实例。你把这个新的Car 对象作为copy_instance.buy_car() 的参数,然后返回新的Person 实例。

你可以返回到cloning_stephen.py ,确保你使用copy.copy() 来复制stephen 。这意味着在创建副本时使用Person.__copy__()

# cloning_stephen.py

import copy

from household import Car, Person

# Create a person who buys a car
stephen = Person("Stephen")
stephen.buy_car(
    Car("BMW", "Series 1")
)

# Log how many miles driven
stephen.drive(100)

print(f"Stephen's mileage is {stephen.car.mileage} miles")

# Let's copy the Person instance
clone = copy.copy(stephen)

print(
    f"The clone's car is a {clone.car.make} {clone.car.model}"
)

print(f"The clone's mileage is {clone.car.mileage} miles")

# Let's check whether the two cars are exactly the same car
print(
    f"Stephen's car is clone's car: {stephen.car is clone.car}"
)

现在,输出结果是。

Stephen's mileage is 100 miles
The clone's car is a BMW Series 1
The clone's mileage is 0 miles
Stephen's car is clone's car: False

克隆仍然有一个不同的Car 实例,但是现在,汽车的里程数从0开始,正如你所期望的那样你已经通过为类定义__copy__() ,创建了一个自定义版本的浅层拷贝。在这种情况下,你决定当你复制一个Person ,新的实例有它自己的汽车,它的起点是0英里。

在更复杂的类中,如果你想在你的 Python 程序中区分浅拷贝和深拷贝,你可能想同时定义__copy__()__deepcopy__()

最后的话

这里是对你在本文中所涉及的关键点的一个总结。

  • 你创建了简单列表和其它数据结构的拷贝
  • 你创建了更复杂的列表的拷贝
  • 你使用了copy 内置模块
  • 你了解了Python 中浅层拷贝和深层拷贝之间的区别。
  • 你用 __copy__()来定义如何浅层复制一个用户定义的类的对象。

你现在已经准备好安全地拷贝任何对象,知道在对象引用其他对象时应该注意什么。

附录:你不能复制一个不可变的对象

你还记得你在文章前面对一个元组使用copy.copy() 吗?与你复制列表和字典时不同的是,你得到了一个新的实例,其中包含与原来相同的值,而当你试图复制一个元组时,你得到的是相同的实例。

每当你把一个不可变的对象传递给copy.copy() ,它就会返回对象本身。