总会踩一次 Python 引用与拷贝的坑
本文最后更新于:2022年12月30日 晚上
起因是在写刻晴办公桌新功能本地数据保存的时候,需要加载与更新数据,然后就莫名踩了一次动态数据类型的坑。正好借着这个机会整理一下 Python 中的数据类型,以及引用和拷贝相关的知识。虽说网上总结很多,但这一次想要自己记录一遍。
Python 的数据类型与赋值
Python 中变量不需要声明,而是用赋值(=)的方式创建。某种意义上,变量就像指针(但是这个指针也没有类型),指向内存中具体的数据,而赋值就是完成「指向」这一操作。如果等号右边是具体的数据,也顺便完成了数据在内存中的创建操作。
虽说变量没有类型一说,但是具体的数据 Python 还是有类型的,Python 3 有 6 种标准数据类型——Number(数字)、String(字符串)、Tuple(元组)、List(列表)、Set(集合)和 Dictionary(字典)。其中前三种是静态数据类型(不可变),后三种是动态数据类型(可变),具体每一种数据类型的含义不再继续展开。可变与不可变的含义为是否可对该内存中的数据进行修改。
引用与拷贝
从本质上讲,赋值操作即是一种引用,前面提到的「指向」和「引用」含义是一致的。除了初始化这种建立变量到内存数据的引用,对旧变量重新赋值、变量之间互相赋值也是常见的引用,比如b = a
。
重新赋值是把就旧变量指向新的数据,而引用变量并不是再创建一份数据,而是把新变量指向赋值变量引用的数据。
以上对于静态数据和动态数据(整体)都是成立的,但是动态数据包含索引操作,比如需要改变列表中第一个数值,就会用到a[0] = xx
这样的操作。前面提到动态数据是可变的,索引赋值就是改变同一内存地址数据的操作。所以如果a, b
同时引用了一个列表数据,而对b
进行了索引赋值修改,由于a
还是指向了同一个已修改的数据,所以会出现反直觉的a
也修改了数据的情况。
补充一下代码及输出:
1 |
|
不过这样引用肯定会遇到需求上的问题——比如有一份原始数据,现要拿出一部分修改/添加,经过校验后返回数据。因为引用数据的修改直接修改了原数据,自然是不妥的。所以需要引入拷贝的概念,即创建一份新内存地址的数据,数据内容和原来一样,但互相独立。
浅拷贝与深拷贝
数据类型可以嵌套,比如列表中套列表。浅拷贝仅拷贝第一层数据,嵌套内的数据还是指向原地址;而深拷贝会将所有嵌套结构都拷贝一份,真正做到两个数据完全独立。
浅拷贝的方法是切片操作(a[:]
)、数据类型自带的copy()
方法或是copy
模块的copy()
方法,深拷贝的方法是copy
模块的deepcopy()
方法。以上概念都过于抽象,建议结合实例进行理解。
实例一:浅拷贝对第一层进行拷贝处理,所以b[0]=[2,2]
并不会对a
进行修改,但b[0][0]=2
由于对第二层修改了数据,a
也就随之变动。
1 |
|
1 |
|
实例二:深拷贝完全独立数据,所以无论b
怎么修改,a
不会变动。
1 |
|
再补一张大佬做的图帮助理解(切片也是浅拷贝哦):
踩坑场景回顾
回到自己的项目和踩坑时的场景:因为要做圣遗物数据导出与读取功能,避免不了一些本地数据的增删改查操作(实际上删还没做),于是顺利踩坑。当初觉得这种互相引用的方式很蠢,自己写代码的时候肯定会避免这种误导性写法,结果蠢的还是自己。
1 |
|
等号右侧是读取了所有圣遗物保存方案中的一个,数据类型是字典,其中self.archive.currentText
是下拉选择框中的方案名称。选择某一个选项相当于读取了数据,并呈现相关数据结果,也即实时数据。因为本工具实时数据结果就是self.artifact
,所以当进行了新的识别后,数据就自动更新到self.artifacts
这个所有的方案数据中了。
当初搜遍整个文档,debug 许久,也不明白究竟自己对self.artifacts
的数据更新是在哪里完成的,直至回想起 Python 的数据类型。解决方案也非常简单,由于直接对最深层进行拷贝,也不会有浅拷贝与深拷贝相关问题。
1 |
|
总结
尽管自己可能并不是那么适合做基础技术科普,讲得可能没那么专业全面,可能还有潜在错误(如果有的话请尽快通知我),不过本文的主要目的还是给自己长个记性。
如果下次遇到了莫名其妙的数据突然就被更改了的情况,可能就是在引用和拷贝上踩了坑,不论是我还是身为读者的你。