问题
- 需要拷贝一个对象时,意外共享引用对象,导致未预知的bug。
scss
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。
ini
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验证一下。
浅拷贝
- 使用
copy.copy()
函数
scss
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]]
- 切片操作:对于列表、元组等序列类型的对象,可以使用切片操作符来创建一个浅拷贝。
scss
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]]
-
构造函数
- 列表(list):
list()
构造函数可以用来创建一个与原列表相同的新列表。 - 元组(tuple):
tuple()
构造函数可以用来创建一个与原元组相同的新元组。 - 字典(dictionary):
dict()
构造函数可以用来创建一个与原字典相同的新字典。
- 列表(list):
scss
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__
魔法方法
python
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
深拷贝
scss
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]]
结束语
-
浅拷贝错误使用是导致意外共享对象引用的一种原因。除此之外:
- 参数传递:当将可变对象作为参数传递给函数时,函数内对该对象的修改可能会影响到原始对象。
- 全局变量: 如果在一个模块中定义了一个全局变量,并且其他模块也引用了这个全局变量,那么对这个全局变量的修改会影响到所有引用它的模块。