likes
comments
collection
share

简明扼要!理解Python浅拷贝和深拷贝

作者站长头像
站长
· 阅读数 10

问题

  • 需要拷贝一个对象时,意外共享引用对象,导致未预知的bug。
import copy

original_list = [1, 2, [3, 4]]
copied_list = copy.copy(original_list)

copied_list[2].append(5)
print(original_list)  # 原始列表也被修改了:[1, 2, [3, 4, 5]]

基本类型和引用类型

在编程中,通常将数据类型分为两种主要类别:基本类型(Primitive Types)和引用类型(Reference Types)。

  • 基本类型是指存储单个值的简单数据类型,它们通常由编程语言直接支持,并在内存中按值进行存储。常见的基本类型包括整数(integers)、浮点数(floats)、布尔值(booleans)和字符(characters)等。在Python中,整数、浮点数、布尔值、字符串等都属于基本类型。
  • 引用类型是指存储引用(指针)而不是实际数据值的数据类型。引用类型的变量存储的是指向内存中实际数据的引用,而不是数据本身。常见的引用类型包括列表(lists)、字典(dictionaries)、集合(sets)、对象(objects)等。在Python中,列表、字典、集合等属于引用类型。

在Python中,与基本类型不同,引用类型的变量存储的是对象的引用,而不是对象本身。这意味着当你将一个引用类型的变量赋值给另一个变量时,实际上是将引用复制给了新的变量,两个变量指向的是同一个对象。这一点在传递参数和修改对象时尤为重要,因为它影响了对象在内存中的共享和修改行为。

浅拷贝和深拷贝

  • 深浅拷贝都会创造一个新的对象。区别是:

    • 浅拷贝只复制了对象的顶层结构,而不会递归地复制对象中包含的子对象。
    • 深拷贝会创建原始对象及其所有子对象的完全独立副本.
  • 举个栗子:

    • 引用类型对象就像文件的桌面快捷方式。浅拷贝就是只对桌面快捷方式进行拷贝,不拷贝原文件。深拷贝对象会对桌面快捷方式和原文件一起拷贝。

应用场景

  • 当需要对原对象进行拷贝时,何时选择浅拷贝、何时选择深拷贝?

    • 原对象所有属性和子对象只有基本类型,可以只使用浅拷贝
    • 原数据所有属性和子对象含有引用类型,使用深拷贝
  • 目的是为了确保新旧对象相互独立,避免未知bug。

class Person:
    def __init__(self, name, address):
        self.name = name
        self.address = address

person1 = Person("Alice", ["123 Main St", "City"])
person2 = person1

# 原对象属性只有基本类型,使用浅拷贝
import copy
person3 = copy.copy(person1)

# 原数据属性含有引用类型,使用深拷贝
person4 = copy.deepcopy(person1)

实现方式

  • python深拷贝的实现方式就一种(如果还有其他方式欢迎在评论区补充),浅拷贝实现方式则比较多。可以只记住深拷贝,用排查法确认浅拷贝。当然也可以在不确认时,写个demo验证一下。

浅拷贝

  1. 使用copy.copy()函数
import copy
original_list = [1, 2, [3, 4]]
copied_list = copy.copy(original_list)
original_list[-1].append(1)
print(original_list)  # [1, 2, [3, 4, 1]]
print(copied_list)  # [1, 2, [3, 4, 1]]
  1. 切片操作:对于列表、元组等序列类型的对象,可以使用切片操作符来创建一个浅拷贝。
original_list = [1, 2, [3, 4]]
copied_list = original_list[:]
original_list[-1].append(1)
print(original_list)  # [1, 2, [3, 4, 1]]
print(copied_list)  # [1, 2, [3, 4, 1]]
  1. 构造函数

    1. 列表(list):list()构造函数可以用来创建一个与原列表相同的新列表。
    2. 元组(tuple):tuple()构造函数可以用来创建一个与原元组相同的新元组。
    3. 字典(dictionary):dict()构造函数可以用来创建一个与原字典相同的新字典。
original_list = [1, 2, [3, 4]]
copied_list = list(original_list)
original_list[-1].append(1)
print(original_list)  # [1, 2, [3, 4, 1]]
print(copied_list)  # [1, 2, [3, 4, 1]]

original_list = [1, 2, [3, 4]]
copied_list = tuple(original_list)
original_list[-1].append(1)
print(original_list)  # [1, 2, [3, 4, 1]]
print(copied_list)  # (1, 2, [3, 4, 1])

original_list = {1:1, 2:2, 3:[3, 4]}
copied_list = dict(original_list)
original_list[3].append(1)
print(original_list)  # {1: 1, 2: 2, 3: [3, 4, 1]}
print(copied_list)  # {1: 1, 2: 2, 3: [3, 4, 1]}
  • __copy__魔法方法
import copy

class MyClass:
    def __init__(self, value):
        self.value = value

    def __copy__(self):
        # 创建一个新的实例并将属性值复制过去
        new_instance = type(self)(self.value)
        return new_instance

obj1 = MyClass(10)
obj2 = copy.copy(obj1)  # 调用 __copy__() 方法创建浅拷贝
print(obj2.value)  # 输出:10

深拷贝

import copy

original_list = [1, 2, [3, 4]]
copied_list = copy.deepcopy(original_list)
original_list[-1].append(1)
print(original_list)  # [1, 2, [3, 4, 1]]
print(copied_list)  # [1, 2, [3, 4]]

结束语

  • 浅拷贝错误使用是导致意外共享对象引用的一种原因。除此之外:

    • 参数传递:当将可变对象作为参数传递给函数时,函数内对该对象的修改可能会影响到原始对象。
    • 全局变量: 如果在一个模块中定义了一个全局变量,并且其他模块也引用了这个全局变量,那么对这个全局变量的修改会影响到所有引用它的模块。