目录

1. Python 赋值

  • 1.1 问题引入
  • 1.2 问题刨析

2. 浅拷贝与深拷贝

  • 2.1 浅拷贝
  • 2.2 深拷贝

3. 在函数参数传递中,赋值带来的引用问题



1. “与 众 不 同” 的 Python 赋 值



问题引入

【code1】以下赋值操作的输出结果是我们都可以接受的:

a = 2
b = a
b = 1
print(a)

Out[i]: 2
在对数值变量的赋值中,b = 1并没改变变量a的值。

【code2】以下对列表赋值的输出让我们意外

a = [1,2,3]
b = a
b[0] = 9
print(a)

Out[i]: [9, 2, 3]

对列表b的第0号元素赋值时,却同时改变了列表a

python 赋值目录 python赋值题目_不可变对象


【code3】其实意外的结果也可以发生在对同一个变量的赋值过程中:

a = 1
print(id(a))
a = a + a
print(id(a))

Out[i]:

140710983841568
140710983841600

python 赋值目录 python赋值题目_python 赋值目录_02

为了一探究竟,我决定替奥特曼深入调查一下【code1】、【code2】中小怪兽们的身份:

  • 在【code1】中,输出a,b的id:
print(id(a),id(b))

Out[i]: 140710983841600 140710983841568
可以看到:a、b不再是相同id,则它们具有不同的内存地址。

  • 在【code2】中,输出a,b的id:
print(id(a),id(b))

Out[i]: 2613318275968 2613318275968
可以看到:a、b仍然是同一id,可知a、b仍引用到同一内存地址。



问题刨析

Python是彻底的面向对象编程语言,从Python对象的角度来理解

研究上面的输出后,学过C++的"童鞋"第一次遇到这种“架势”可能会感到很吃惊和疑惑。
但这时应当注意,虽然也支持面向过程编程的风格,但python是一种彻底的面向对象语言,数值可以看作实例对象,比如数值1就是int类的实例。每个对象都有自己的一块内存地址。对象分为不可变对象可变对象

从本质上看Python中的直接赋值是对象的引用,传递的是对象间的地址而不是值的拷贝。

  • 先看比较容易理解的【code3】:
a = 1        # a的引用为对象1的地址
a = a + a    # 先执行语句a+a得到一个新的对象2,再执行赋值。
             # 赋值时,2是一个新的不可变对象,以前的对象(也就是1)是不可变对象,不会被改变也不能被覆盖
             # 因此a最终引用到不可变对象2的内存地址

这个过程可以绘制一个图来描述:

python 赋值目录 python赋值题目_python_03

这就是【code3】中,变量a的id发生改变的原因。

  • 同理再看【code1】

在【code1】中,数值对象是不可变对象,在被创造之后,它的状态就不可以被改变。但尽管对象本身不可变,但变量的对象引用是可变的

a = 2    # 变量a引用到不可变对象2的地址
b = a    # b通过a的引用到2
b = 1    # b引用到另外一个对象,即数值常量1在这条语句时有系统分配的地址

这个过程可以绘制一个图来描述:

python 赋值目录 python赋值题目_Python_04

b的引用改变了,指向了另外一个不可变对象,也就是1的地址。最终a、b具有不同的内存地址。

  • 最后看【code2】

在【code2】中,列表是可变对象,对象的内容是可变的。在Python中这种可变对象一般是复合数据结构,如list、dict、tuple、set等等,其内容是可变的原因在于它们都是复合数据结构,每一个内容元素本身也是一个对象。

a = [1,2,3]  # a 获得了单条语句执行时系统分配的列表[1,2,3]的首地址(python是脚本语言)
b = a        # b引用到与a同一列表的首地址!
b[0] = 9     # 使变量b引用的列表(也是变量a引用的列表)的第一个元素引用到不可变对象9

也就是这种原因,【code2】中可变对象a在赋值时geib时,ba引用到同一对象,而赋值语句b[0] = 9仅仅是对这个数组对象的子对象b[0]的引用进行了改变,并没有改变变量b本身的引用关系
不信我们可以做如下验证:

a = [1,2,3] 
b = a
print("id(a)=",id(a),"id(b)=",id(b))
print("id(a[0])=",id(a[0]),"id(b[0])=",id(b[0]))
b[0] = 9 
print("\nid(a)=",id(a),"id(b)=",id(b))
print("id(a[0])=",id(a[0]),"id(b[0])=",id(b[0]))

Out[i]:

id(a)= 2613322289408 id(b)= 2613322289408
id(a[0])= 140710983841568 id(b[0])= 140710983841568

id(a)= 2613322289408 id(b)= 2613322289408
id(a[0])= 140710983841824 id(b[0])= 140710983841824

可以看到,a与b的id始终没变,但它们随引用的这个数组的第一个元素的引用却同时改变了,完全符合我以上所述。

其它类似疑惑

在理解了以上问题后就会理解一些类似的操作在Python简直就是扯淡,比如有人做如下谜一样的操作:

a = [1,2,3] 
a[0]=a
a

Out[i]:

[[...], 2, 3]

这结果他自己也看不懂了,就是觉得是谜一样的结果。在这个过程中,很显然没有理解到python中的赋值操作,使得列表a的头一个元素无线迭代指向原列表a自身,这样的代码几乎是没有意义的。



2. 浅拷贝与深拷贝


Python 的赋值语句不复制对象,而是创建目标和对象的绑定关系。对于自身可变,或包含可变项的集合,有时要生成副本用于改变操作,而不必改变原始对象。


2.1 浅拷贝(浅层赋值 shallow copy)

所谓浅拷贝它仅仅拷贝父对象,不会拷贝对象的内部的子对象。即浅层复制 构造一个新的复合对象,然后(在尽可能的范围内)将原始对象中找到的对象的 引用 插入其中。



2.2 深拷贝(深度复制 deep copy)

copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。即深层复制 构造一个新的复合对象,然后,递归地将在原始对象里找到的对象的 副本 插入其中。

深度复制操作通常存在两个问题, 而浅层复制操作并不存在这些问题:

  • 递归对象 (直接或间接包含对自身引用的复合对象) 可能会导致递归循环。
  • 由于深层复制会复制所有内容,因此可能会过多复制(例如本应该在副本之间共享的数据)。


3.在函数参数传递中,赋值带来的引用问题