Python值传递和引用传递分析

自己以前用Python手写聚类算法代码时常常会操作列表并进行相应修改,发现自己进行列表操作时往往会得到十分奇怪的结果。上网一查才发现这是一个非常复杂的问题,因此在这里系统地总结一下。本文参考以下博客,在此表示感谢:

Python值传递和引用传递(详细分析)

Python浅拷贝与深拷贝详解

1. Python的数据类型

数据类型用来定义编程语言中相同类型数据的存储形式,即决定如何将不同的数据存储到计算机的内存中,不同数据类型的数据,其存储形式和存储位置也都不同。需要注意的是:不同编程语言对于数据类型的定义均不相同。

Python的数据类型包括两类,即:

  • 可变数据类型:List、Dict、Set等
  • 不可变数据类型:String、Number(int、float等)、Tuple等

变量和不同数据类型在计算机内存中的存储位置如下图所示:

在Python中,所有变量都保存在栈内存中,而所有不同数据类型的数据(每个数据都是一个对象)都保存在堆内存中。这与Java等语言是有较大差异的。

2. 变量赋值

2.1 不可变对象的赋值

看以下代码:

1
2
3
4
5
6
7
8
9
10
a = 1  # Number
b = a
c = (1, 2, 3) # Tuple
d = c
print(id(a), id(b)) # 结果1:id相同
print(id(c), id(d)) # 结果2:id相同
a = a + 1
print(id(a), id(b)) # 结果3:id不相同(a=2, b=1)
e = 1
print(id(a), id(b), id(e)) # 结果4:id均相同

对于结果1和结果2,变量ac首先被分别指向创建的新对象1(1, 2, 3),之后b = ad = c又分别使变量bd指向了对象1(1, 2, 3)

对于结果3,这是由于语句a = a + 1则创建了一个新对象2,并使变量a重新指向了2。可以看出:对于不可变对象,重新赋值等操作都不会影响其他变量的值。

对于结果4,可以看出:Python中的对象可以同时被多个变量引用。在对不可变对象赋值时,它会首先在堆空间中寻找是否存在该对象,若存在则直接指向该对象(即相同的不可变对象只有1个)。

2.2 可变对象的赋值

看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
l1 = [1, 2, 3]  # List
l2 = l1
print(l1, l2) # 结果1:[1, 2, 3] [1, 2, 3]
print(id(l1), id(l2)) # 结果1:id相同

l2.append(4)
print(l1, l2) # 结果2:[1, 2, 3, 4] [1, 2, 3, 4]
print(id(l1), id(l2)) # 结果2:id相同

l2.pop()
print(l1, l2) # 结果3:[1, 2, 3] [1, 2, 3]
print(id(l1), id(l2)) # 结果3:id相同

l2[0] = 2
print(l1, l2) # 结果4:[2, 2, 3] [2, 2, 3]
print(id(l1), id(l2)) # 结果4:id相同

l2 = l2 + [4]
print(l1, l2) # 结果5:[2, 2, 3] [2, 2, 3, 4]
print(id(l1), id(l2)) # 结果5:id不同

l3 = [1, 2]
l4 = [1, 2]
print(id(l3), id(l4)) # 结果6:id不同

观察结果2-结果4可以发现,当两个变量同时指向一个可变对象时,对其中一个变量的操作会影响到另一个变量。这是因为可变对象在被修改时并不会像可变对象一样创建一个新的对象并将变量指向该对象,而是直接在原对象上进行修改。由于两个变量都指向该变量,因此两者的值都会随之变化。

而结果5与结果2-结果4均不同,可以看到两个变量不再指向同一个对象,因此两者的值也发生了变化。这是因为l2 = l2 + [4]创建了一个新的列表对象,并使变量l2指向了这个对象。但是这里需要注意,如果语句改为l2 += [4],则其结果与结果2-4是相同的,这是因为该语句并没有创建新的对象。

结果6和不可变对象中的结果4形成对比,对于可变对象,可以创建多个完全相同的不同对象(很好理解,因为对象是可变的,当前相同不代表以后一直会相同),因此l3l4并不相同。

2.3 变量删除

Python中可以删除变量,但是对象无法删除,如以下代码:

1
2
l = [1, 2, 3]
del l

删除变量l后将无法访问,但是对象[1, 2, 3]仍然存在。Python程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果对象除了被删除变量外还被其他变量引用,就不会被回收,反之则会被回收。

2.4 参数传递

变量赋值的最常见应用是函数的参数传递。Python中的参数传递是赋值传递(pass by assignment),或者叫作对象的引用传递(pass by object reference)。也就是说,Python在参数传递时,是让形参和实参指向相同的对象(类似于b = a,其中b为形参,a为实参)。

因此,参数传递和变量赋值一样,需要分为不可变对象的传递和可变对象的传递。显而易见,如果实参为不可变对象,则无论在函数中如何修改形参的值,都不会影响实参;然而如果实参为可变对象,则在函数中对形参的修改,将影响到实参。

2.5 变量赋值总结

在Python语言中,变量赋值具有以下特点:

  • 变量赋值,只是令变量指向了某个数据的对象,并不是将该对象拷贝给变量,一个对象可以被多个变量指向;
  • 可变对象的变化会影响所有指向该对象的变量;而所有指向不可变对象的值总是一样的,不会改变,但是可以通过某些操作(+=等)返回一个新的对象;
  • 函数的参数传递中的形参和实参类似于变量赋值,变化规律和变量赋值一样,分为可变对象和不可变对象。

3. 多层对象赋值对比

在Python中,对象内部可以嵌套其他对象,例如列表内的元素还是一个列表。这种多层对象的赋值对比如下:

3.1 外层不可变,内层存在可变

看以下代码:

1
2
3
4
5
6
l1 = ([1, 2, 3], 2, 3)
l2 = ([1, 2, 3], 2, 3)
l3 = l2
print(id(l1), id(l2), id(l3)) # 结果1:l2和l3的id相同,l1不同
print(id(l1[0]), id(l2[0]), id(l3[0])) # 结果2:l2和l3的id相同,l1不同
print(id(l1[1]), id(l2[1]), id(l3[1])) # 结果3:三者id均相同

可以看到,在这种情况下得到的结论与可变对象相同,即在创建完全相同的新对象时,不会引用之前的对象地址,而是创建一个全新的对象,且其中的每一个可变对象元素都是全新的,而对于赋值操作则仍然会拷贝源对象的地址。其引用关系如下图所示:

3.2 外层不可变,内层不可变

看以下代码:

1
2
3
4
5
6
7
l1 = ((1, 2, 3), (1, 2, 3), 3)
l2 = ((1, 2, 3), (1, 2, 3), 3)
l3 = l2
print(id(l1), id(l2), id(l3)) # 结果1:id均相同
print(id(l1[0]), id(l2[0]), id(l3[0])) # 结果2:id均相同
print(id(l1[1]), id(l2[1]), id(l3[1])) # 结果3:id均相同
print(id(l1[2]), id(l2[2]), id(l3[2]), id(3)) # 结果4:id均相同

可以看出,当外层和内层均为不可变对象时,其整体可以视为是一个不可变对象,其在内存中就只有一个,是不会创建新的对象的。其引用关系如下图所示:

3.3 外层可变,内层存在可变

看以下代码:

1
2
3
4
5
6
7
8
l1 = [[1, 2, 3], [1, 2, 3], 3]
l2 = [[1, 2, 3], [1, 2, 3], 3]
l3 = l2
print(id(l1), id(l2), id(l3)) # 结果1:l2和l3的id相同,l1不同
print(id(l1[0]), id(l1[1])) # 结果2:id不同
print(id(l1[0]), id(l2[0]), id(l3[0])) # 结果3:l2和l3的id相同,l1不同
print(id(l1[1]), id(l2[1]), id(l3[1])) # 结果4:l2和l3的id相同,l1不同
print(id(l1[2]), id(l2[2]), id(l3[2])) # 结果5:id均相同

可以看出,当外层可变时,就符合可变对象的所有特征。对于内层相同的可变对象,都会创建一个新的对象。其引用关系如下图所示,相比文字更加清晰:

4. 赋值、浅拷贝和深拷贝

在经过前三章的铺垫后,我们终于可以介绍Python中赋值、浅拷贝和深拷贝三者之间的区别:

  • 赋值:将一个变量对于某个对象的引用赋给另一个变量,前面三章都是赋值
  • 浅拷贝:拷贝所有子对象的引用,而不拷贝子对象的内容,语法为copy.copy()
  • 深拷贝:拷贝所有子对象的引用和内容,这是真正意义上的“复制”,会创建一个全新的对象,语法为copy.deepcopy()

因此,对于单层对象,浅拷贝和深拷贝的使用没有任何区别。而对于多层对象,由于浅拷贝只拷贝了子对象的引用,因此对于子可变对象的操作会影响到其它变量,但是这种影响是局部的,仅存在于子可变对象中,而对于最外层对象的操作(如添加、删除元素等)都不会影响到其它变量。


Python值传递和引用传递分析
https://princehao.cn/posts/blog005/
作者
王子豪
发布于
2023年5月8日
许可协议