在 Python 中,一切皆对象,变量本质上保存的是对象的引用。因此在理解复制行为时,需要区分“对象本身”和“对象引用”。
两个变量指向同一个对象,修改其中一个变量的值会影响另一个变量的值。
61nums1 = [10, 20, 30, 40]2nums2 = nums13print(f"nums1的地址为:{id(nums1)}") # nums1的地址为:43004389124print(f"nums2的地址为:{id(nums2)}") # nums2的地址为:43004389125print(f"nums1里的元素为:{nums1}") # nums1里的元素为:[10, 20, 30, 40]6print(f"nums2里的元素为:{nums2}") # nums2里的元素为:[10, 20, 30, 40]在内存中的逻辑图示:

修改其中一个变量的值:
61# 修改nums2[3] = 992nums2[3] = 993print(f"修改之后的nums1的地址为:{id(nums1)}") # nums1的地址为:43004389124print(f"修改之后的nums2的地址为:{id(nums2)}") # nums2的地址为:43004389125print(f"修改之后的nums1里的元素为:{nums1}") # 修改之后的nums1里的元素为:[10, 20, 30, 99]6print(f"修改之后的nums2里的元素为:{nums2}") # 修改之后的nums2里的元素为:[10, 20, 30, 99]修改之后的内存中的逻辑图示:

这里我们需要注意的是,虽然我们修改了nums2的值,但是nums1的值也被修改了。这是因为变量nums1和变量nums2指向同一个实例对象(可变对象),因此它们看到的列表内容也是一样的。但是,list 里的元素是 int 类型(不可变对象)。当我们执行 nums2[3] = 99 时,并不会创建新的 list,而是把 list 中第 3 个位置原本指向 40 的引用,替换为指向 99。由于 nums1 和 nums2 指向的是同一个 list 对象,所以通过 nums2 修改 list 后,nums1 看到的内容也会发生变化。
另外需要注意,因为 list 是可变类型,所以在对 list 进行元素修改时,通常不会创建新的 list 对象,而是直接在原来的 list 对象上进行修改。(这里只是简单说明可变类型和不可变类型的区别)
浅拷贝是创建一个新的对象,但它包含的元素仍然是对原始对象中元素的引用。如果元素是可变对象,修改其中一个对象内部的元素,可能会影响另一个对象。
651import copy2def show_list(list_name, list_demo, tab_count=0):3 print("\t" * tab_count, f"{list_name}={list_demo}")4 print("\t" * tab_count, f"{list_name} 的id是:{id(list_demo)}")5 print("\t" * tab_count, f"{list_name} 的各个元素的id是:")6 for index, element in enumerate(list_demo):7 # print("\t" * tab_count,f"{element} = {id(element)}")8 if isinstance(element, list):9 show_list(f"{list_name}[{index}]", element, tab_count + 1)10 else:11 print("\t" * (tab_count + 1), f"{element} = {id(element)}")12
13nums1 = [10, 20, 30, 40]14nums2 = copy.copy(nums1)15# 打印列表元素、列表地址、及其元素地址16show_list("nums1", nums1)17print()18show_list("nums2", nums2)19print()20
21"""22运行结果:23 nums1=[10, 20, 30, 40]24 nums1 的id是:433325875225 nums1 的各个元素的id是:26 10 = 434643808027 20 = 434643840028 30 = 434643872029 40 = 434643904030
31 nums2=[10, 20, 30, 40]32 nums2 的id是:433341990433 nums2 的各个元素的id是:34 10 = 434643808035 20 = 434643840036 30 = 434643872037 40 = 434643904038"""39# 修改nums2[3] = 9940nums2[3] = 9941print(f"{"*" * 10}修改nums2后{"*" * 10}\n")42# 打印列表元素、列表地址、及其元素地址43show_list("nums1", nums1)44print()45show_list("nums2", nums2)46print()47"""48运行结果:49**********修改nums2后**********50 nums1=[10, 20, 30, 40]51 nums1 的id是:433325875252 nums1 的各个元素的id是:53 10 = 434643808054 20 = 434643840055 30 = 434643872056 40 = 434643904057
58 nums2=[10, 20, 30, 99]59 nums2 的id是:433341990460 nums2 的各个元素的id是:61 10 = 434643808062 20 = 434643840063 30 = 434643872064 99 = 434644092865"""在内存中的逻辑图示:

修改之后的内存中的逻辑图示:

我们可以看出,虽然 nums1 和 nums2 是两个不同的列表对象(通过浅拷贝,它们的地址不同),但是它们里面的元素是相同的(它们的元素地址相同)。当我们修改 nums2[3] 的值时,实际上是修改了 nums2 列表中的一个元素,而这个元素在内存中是一个不可变对象(整数),所以会创建一个新的整数对象 99,并将 nums2[3] 的引用指向这个新的对象。由于 nums1[3] 仍然指向原来的整数对象 40,所以 nums1 中的元素没有发生变化。
那我们就能引发一个问题:上面我们想修改nums2[3]元素的值,我们发现它是int类型的数据,是不可变的,才有了上面的效果,那么如果 nums1 和 nums2 中的元素是可变对象(比如列表),那么修改其中一个对象的元素会不会影响另一个对象呢?
这就引出了深拷贝的概念。
深拷贝会创建一个 新的对象,并且 递归复制原对象中的所有子对象。
841import copy2# 定义函数3def show_list(list_name, list_demo, tab_count=0):4 print("\t" * tab_count, f"{list_name}={list_demo}")5 print("\t" * tab_count, f"{list_name} 的id是:{id(list_demo)}")6 print("\t" * tab_count, f"{list_name} 的各个元素的id是:")7 for index, element in enumerate(list_demo):8 # print("\t" * tab_count,f"{element} = {id(element)}")9 if isinstance(element, list):10 show_list(f"{list_name}[{index}]", element, tab_count + 1)11 else:12 print("\t" * (tab_count + 1), f"{element} = {id(element)}")13
14# 定义列表15nums1 = [10, 20, 30, [40, 50]]16nums2 = copy.deepcopy(nums1) # 深拷贝17
18# 打印列表元素、列表地址、及其元素地址19show_list("nums1", nums1)20print()21show_list("nums2", nums2)22print()23"""24运行结果:25 nums1=[10, 20, 30, [40, 50]]26 nums1 的id是:434601920027 nums1 的各个元素的id是:28 10 = 435726790429 20 = 435726822430 30 = 435726854431 nums1[3]=[40, 50]32 nums1[3] 的id是:434585804833 nums1[3] 的各个元素的id是:34 40 = 435726886435 50 = 435726918436
37 nums2=[10, 20, 30, [40, 50]]38 nums2 的id是:434806726439 nums2 的各个元素的id是:40 10 = 435726790441 20 = 435726822442 30 = 435726854443 nums2[3]=[40, 50]44 nums2[3] 的id是:434806745645 nums2[3] 的各个元素的id是:46 40 = 435726886447 50 = 435726918448"""49# 修改nums2[3][0] = 9950nums2[3][0] = 9951print(f"{"*" * 10}修改nums2后{"*" * 10}\n")52# 打印列表元素、列表地址、及其元素地址53show_list("nums1", nums1)54print()55show_list("nums2", nums2)56print()57"""58运行结果:59**********修改nums2后**********60
61 nums1=[10, 20, 30, [40, 50]]62 nums1 的id是:434601920063 nums1 的各个元素的id是:64 10 = 435726790465 20 = 435726822466 30 = 435726854467 nums1[3]=[40, 50]68 nums1[3] 的id是:434585804869 nums1[3] 的各个元素的id是:70 40 = 435726886471 50 = 435726918472
73 nums2=[10, 20, 30, [99, 50]]74 nums2 的id是:434806726475 nums2 的各个元素的id是:76 10 = 435726790477 20 = 435726822478 30 = 435726854479 nums2[3]=[99, 50]80 nums2[3] 的id是:434806745681 nums2[3] 的各个元素的id是:82 99 = 435727075283 50 = 435726918484"""在内存中的逻辑图示:

在修改之后的内存中的逻辑图示:

通过上面的代码,我们可以看出,nums1 和 nums2 是两个不同的列表对象(通过深拷贝,它们的地址不同),并且其中的可变子对象也会被复制,因此它们引用的子对象地址也不同。当我们修改 nums2[3][0] 的值时,实际上是修改了 nums2 列表中的一个元素(一个列表),由于这个元素在内存中是一个可变对象(列表),所以会直接修改这个列表对象中的元素,而不会创建新的对象。由于 nums1[3] 仍然指向原来的列表对象,所以 nums1 中的元素没有发生变化。
非容器类型(如数字、字符串、和其他“原子”类型的对象)无法拷贝
元组变量如果只包含原子类型对象,则不能对其深拷贝
在实际编程过程中,我们其实一直都在接触“拷贝”相关的概念。例如在函数传参时,Python 传递的本质是 对象的引用(也可以理解为对象引用的拷贝)。
如果参数是 可变对象(例如 list、dict 等),在函数内部对对象内容进行修改时,可能会影响函数外部的原对象。
如果参数是 不可变对象(例如 int、str、tuple 等),由于对象本身不可修改,在函数内部进行“修改”时实际上会创建新的对象,因此不会影响外部变量。
因此,在处理可变对象时,如果不希望函数内部的操作影响原始数据,就需要考虑是否使用 浅拷贝或深拷贝 来创建独立的数据副本。
总体来说,Python 中变量保存的是对象的引用,而不是对象本身。因此理解“引用关系”和“对象是否可变”,是理解直接赋值、浅拷贝和深拷贝的关键。(:D 可以看看切片他的底层是什么实现的嘞~)