Python深拷贝与浅拷贝

Xilong Yang
2021-05-15

自从上次略略学习了一些python基础就没怎么用过python了,这次遇到了深浅拷贝问题,在此记录。

引子

考虑下面代码:

list1 = []
list2 = list1
list3 = list2

while (some condition):
    list1 = some value
    ...
    if (some condition):
        list2 = some value
    if (f(list3) < f(list2)):
        list3 = list2
        
print(list3)

这段代码试图在一些复杂运算中取出最优结果,并将其保存在list3中。由于平时基本是使用C++写程序,我设想它等效于以下C++代码:

vector<T> list1;
vector<T> list2 = list1;
vector<T> list3 = list1;

while (some condition) {
    list1 = some value;
    ...
    if (some condition) {
        list2 = some value;
    }
    if (f(list3) < f(list2)) {
        list3 = list2
    }
}
//void print(const vector<T>&), 打印vector中所有元素
print(list3)

作为一个与C++相爱相杀近两年的人,我很确信这段代码能完成任务,事实上它的确能。但python代码却总是出错,于是我试图在list3唯一可能被修改的地方打印出它的值。

...
if (f(list3) < f(list2)):
    list3 = list2
    print(list3)
...
print("final:")
print(list3)

令人匪夷所思的事情出现了,输出结果居然是:

[aaaaaaaaa]
[bbbbbbbbb]
[ccccccccc]
....
[xxxxxxxxx]
final:
[yyyyyyyyy]

最后一次赋值后的值居然跟最后输出的值不一样?!几经周折了解到,Python中存在深拷贝与浅拷贝的问题。

什么是深拷贝与浅拷贝

简单说,深拷贝就是新建一块内存空间,并将原内存空间中的数据拷贝到新的内存空间中。而浅拷贝不创建内存空间,只令对象引用已存在的内存空间。类比C++的指针:

T *a = new T();
T *b;
b = a; // 浅拷贝
b = new T(*a); // 深拷贝

这里需要注意,不存在指针的语言中(如Python, Java)引用的含义和C++是不同的,更接近于C++中的指针。C++中的引用只是变量的别名,是不可以更改指向的变量的。而Python中的引用更像是一个自动的指针,可以取值也可以改变所指,并且无需显式指定操作,由语言情景决定改执行哪种操作。

Python的对象机制

由于python中不存在指针和引用,也就无法像C++那样自由选择值传递或传引用传递。为了避免操作大对象时的巨大开销,python的应对方法是——一切皆引用。即,每个变量实际上都是引用类型,赋值(=)号通常并不新建对象,而是让变量的引用指向指定的地址。

也就是说,上面的程序中list1,list2和list3实际上是同一个对象的不同引用,验证如下:

list1 = []
list2 = list1
list3 = list1
print(id(list1), " " ,id(list2), " ", id(list3)

可以看出它们的地址相同,即为同一个对象。那么理所应当地,我们通过哪一个引用改变变量的值都会更改这唯一的地址空间。

这里你可能对一切皆引用这个说法有所质疑,并提出如下例子:

a = 5
b = a
b = 3
print(a, id(a))
print(b, id(b))

输出:

5 139656604350896
3 139656604350832

这输出似乎与一切皆引用有出入,因为更改b的值既没有影响a的值,a和b的地址也不一样。这里的核心是:语句b = 3的语义真的是更改b的值吗?我们对这个例子稍作改动:

a = 5
b = a
print(id(a), " ", id(b))
b = 3
print(id(b), " ", id(3))

输出:

140143427922352   140143427922352
140143427922288   140143427922288

没错,常量3也是一个引用。只不过无法改变它的指向。这里一个事实已经呼之欲出,赋值运算符(=)只改变引用的指向。即只能用作浅拷贝。

如何进行深拷贝

那么如何进行深拷贝呢?对一个列表,我们可能会想要使用切片创建新的内存空间:

list1 = [1,2,3]
list2 = list1[:]
print(id(list1), " ", id(list2))

这里可以看出list2已经拥有了新的内存空间。但这种方法在处理多层列表的时候,会出现问题:

list1 = [[1], [2], [3]]
list2 = list1[:]
list1[0][0] = 2
print(list2[0][0])

list2的值还是被list1的修改改动了,这是因为list2虽然和list1没有指向同一块地址空间。但其中第一个元素却指向了同一块地址空间。

因此,深拷贝唯一指定方法:copy.deepcopy()

© 2019- Xilong Yang | CC BY-NC 4.0 | Powered by LaTeX.css, Prism, MathJax