python中的引用、浅拷贝与深拷贝

44 阅读7分钟

0. 前言

虽然python中有“引用”这一术语,实际上这是不准确的,python中不存在对左/右值的绑定操作,故不存在左值引用和右值引用。(详细的内容参考对比 C++ 和 Python,谈谈指针与引用,是python猫公众号的文章,需要一定的C语言基础。) 那么为什么python中又会存在“引用”这一术语呢?

1. python中的对象

在python3中,万物皆对象。对象包括变量、函数、类、容器等。 对象存储在内存中的值,解释器会为其分配一定的内存空间,并决定什么数据可以被存储在内存中。

a = 1  # 创建了一个整型对象a,a的值为1
b = list()  # 创建了一个列表对象b,列表内容为空

2. 对象的引用计数

在python中,值是靠引用传递来的。 我们先来看2段代码

a = 1
b = a
print(b)
a = 2
print(b)  # 这时候b等于多少呢?
a = [1, 2]
b = a
print(b)
a.append(3)
print(b)  # 那么这个时候b又是什么呢

It counts how many different places there are that have a reference to an object.When an object’s reference count becomes zero, the object is deallocated 记录指向对象引用的个数,当变为0时,则被释放。

引用计数时用来统计某个对象被引用的次数,当该对象的引用计数为0的时候,解释器会释放它的内存空间。 变量创建过程

a = 123123
b = a

上面这段代码在内存中是怎么执行的呢? image.png 在执行a = 123123这句代码的时候,解释器给整型数字123123分配了id为2017133300976的内存空间,并将变量a指向了这个地址。

我们这时候来查看一下a的引用计数是多少

import sys
a = 123123
sys.getrefcount(a)  # 2

为什么这时候a的引用计数为2而不是1呢?因为sys.getrefcount(a)函数执行的时候a作为参数传入了。所以sys.getrefcount()函数在统计引用计数的时候会多统计一次。 那么我们现在来看一下b = a这句代码是怎么执行的 我们先来看一下实际程序执行的结果

import sys
a = 123123
b = a  # 将a的值赋值给b
sys.getrefcount(a)  # 这时候a的引用计数为3
# 如果前面调用了id函数以及其他的函数,这里a的引用计数不止为3了。

image.png 实际我们把赋值的2行代码放到pythontutor上看一下执行效果。 image.png

2.1 增加引用计数

  • 对象被创建a = 23
  • 将一个变量的值赋值给另一个变量b = a
  • 对象作为参数传递给函数 str(a)
  • 对象成为容器对象的一个元素 c = [a, b]
  • ...

2.2 减少引用计数

  • 一个本地引用离开了其作用范围,如某个函数执行完成后,局部变量会被释放,作为参数传入的对象的引用计数会减1
  • 对象的别名被显式销毁 del b
  • 对象的一个别名被赋值给其他对象 b = 9
  • 对象被从一个容器对象中移除,c.remove(b)
  • 容器对象本身被销毁 del c
  • ...

3. 浅拷贝与深拷贝

那么我们现在来看一下前面的2段代码

a = 1
b = a
print(b)
a = 2
print(b)  # 这时候b等于多少呢?

a被赋值为2的时候,print(b)这句代码打印出来的b的值为1. image.png image.png 这样的结果很正常,下面的这段代码实际执行结果又是怎样呢?

a = [1, 2]
b = a
print(b)
a.append(3)
print(b)  # 那么这个时候b又是什么呢

image.png 因为a跟b都指向同一个列表对象,所以当列表对象发生变化时,a跟b的值都会发生变化。

3.0 可变对象和不可变对象

要了解这个我们首先需要了解一下可变对象和不可变对象 python3中的标准的数据类型可以分为可变和不可变。

  • 不可变对象:一旦创建就不可修改的对象
    • 该对象所指向的内存中的值不能被修改。当改变某个变量的时候,由于其所指向的值不能被改变,相当于把原来的值赋值一份后再修改,这会开辟一个新的地址,变量再指向这个地址。
  • 可变对象:可以被修改的对象
    • 该对象所指向的内存中的值可以被改变。变量(准确来说是引用)改变后,实际上是其所指向的值直接发生改变,并没有发生复制行为,也没有开辟新的空间,即在原来的内存空间中修改值。

在变量的操作过程中,通常有3种操作:

  • 赋值
  • 浅拷贝
  • 深拷贝

赋值:只是复制了新对象的引用,不会开辟新的内存空间。 赋值并不会产生一个独立的对象单独存在,只是将变量指向了一个内存空间,这个内存空间存放了一个数据。可以理解为打标签。当有多个变量指向同一个内存空间时,一个变量修改了该内存空间的数据,则另一个变量的值也会发生改变。

3.1 浅拷贝

浅拷贝有3种形式:

  • 切片操作
    • 如:list1 = list_data[:]或者data = [i for i in list_data
  • 工厂函数
    • 如:list1 = list(data)
  • copy模块的copy函数
    • 如:data_copy = copy.copy(data)

浅拷贝是对一个对象外层的拷贝,而对于内层,只是拷贝了一个引用而已。如果修改了最内层的数据的时候,情况就不一样了。 浅拷贝要分为2种情况讨论:

  1. 当浅拷贝的对象为不可变对象,如字符串、数字、元组等时,浅拷贝前的对象与浅拷贝后的对象的id不一样,他们指向了两块不同的内存空间。如下图所示。

image.png

  1. 当浅拷贝的对象是可变对象,如列表、字典、集合的时候,浅拷贝的内容只限于外层的对象,对于内层的对象只能拷贝一个引用。这里也可以分为2种情况
    1. 被拷贝的对象中无复杂的子对象,那么对原值修改不会影响浅拷贝后的值
from copy import copy
raw_data = [1, 2, 3]
copy_data = copy(raw_data)
print(raw_data)
print(copy_data)
raw_data.append(4)
print(raw_data)
print(copy_data)
  1. 浅拷贝的对象中有复杂的子对象,例如一个嵌套列表的对象,对于这种对象,浅拷贝的时候只是拷贝了外层的列表,内层的列表只是拷贝了引用而已。实际看代码会更容易明白。

image.png 外层添加元素时,浅拷贝不会随原列表变化而变化;内层添加元素时,浅拷贝才会变化。

3.2 深拷贝

深拷贝和浅拷贝相对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。深拷贝的对象是一个全新的对象,不再与原对象有任何关联。当我们修改原有被复制对象的时候不会对已经复制出来的新对象产生影响。 深拷贝只有一种形式,就是copy模块的deepcopy函数。 我们修改一下上面的代码:

from copy import deepcopy

raw_data = [1, 2, 3, [4, 5]]
copy_data = deepcopy(raw_data)
raw_data[3].append(6)
print(raw_data)  # [1, 2, 3, [4, 5, 6]]
print(copy_data)  # [1, 2, 3, [4, 5]]
copy_data[3].append(8)
print(copy_data)  # [1, 2, 3, [4, 5, 8]]
print(raw_data)  # [1, 2, 3, [4, 5, 6]]

参考链接

上述内容参考了以下文章,如有侵权请联系我删除,非常感谢。 参考引用文章:

  1. www.jianshu.com/p/ecea193ab…
  2. zhuanlan.zhihu.com/p/54011712
  3. zhuanlan.zhihu.com/p/54011712

此文仅做学习分享,如有侵权,请联系我删除,谢谢。