05-进阶必读:引用、拷贝与视图

一年一度的苹果手机发布大会,绝对会牢牢占据微博榜的热搜,虽然“槽点”不断,但是该买的还是得买,下手一点不心软。新款的iPhone Xs Max终于顺利突破了天际,达到了12799元,不愧为神机。我们不妨遐想一下,今年年终奖发了 100000大钞,决定犒劳一下自己和对象,各买了一个同样的苹果手机,配置、颜色、价格、甚至开机密码都设置为一样,绝对的情侣机。那么问题来了,这是两部完全一样的手机吗?

如果把我的手机定义为变量 M, 那么对象的手机定义为 N,那么如何判断二者是否一样呢?

M == N

如果用上述等于判断,那么结果肯定为 True 了。因为这个方法基于的是变量的内容,既然两部手机配置、颜色、功能等等都一样,那肯定是 True。当然了,世界上没有两件完全一模一样的硬件配置,这点大家不要钻牛角。

M is N

如果用 is 判断呢?这个答案好像有点玄机,确实,答案应该是 False。这里是基于同一性来判断的,即我的手机是不是你的手机?显然不是。

上面这个例子很好地解释了拷贝 copy 这个概念,即我的手机和对象的手机互为 copy ,但是我们不一样!

本文将从 Python 的引用、拷贝入手,并深入阐述 Numpy 的拷贝机理,协助大家在数据分析时,能够在数据处理的过程中,对数组变量与变量之间的联系,有更加深刻的认识。

1. Python篇

  • Python 引用

对象的引用就是赋值的过程。我们举个栗子:

a = ["a", "b", "c"]
b = a
a is b
Out:
    True

在上面的栗子里我们把 a 又重新赋值给 b ,那么从 Python 内部机制来看,这个赋值的过程,其实只是又新建了一层引用(这里的引用,类似于指针的概念),建立了从 b 到真实的 list 数据的引用。该过程的原理图如下:

图片描述

如果你说这张图不直观,我给你来这个图:

image-20200815180904194

所以我么在用 is 比较 ab 的时候,是比较其共同指向的 同一块 内存,同一性判断自然为 True 。显然这里我们对 a 进行修改, b 会跟着变化,反之亦然。这就是引用的概念。

判断 2个变量是否具有同一性,还可以用 id() 函数,该方法可以获取对象的内存地址。例如对上述的 2个变量,我们可以做如下操作:

# 在具体的环境中进行内存地址查询时,地址编码一般情况下会不一样
# 但是深层次的规律是一样的,即a和b的内存地址相同
id(a)
Out: 1430507484680
id(b)
Out: 1430507484680
  • Python 的深拷贝与浅拷贝

在这之前,先放一个结论,数字和字符串中的内存都指向同一个地址,所以深拷贝和浅拷贝对于他们而言都是无意义的。也就是说,我们研究深拷贝和浅拷贝,都是针对可变对象进行研究,最常见的情况就是列表。

  1. 深拷贝

所谓的深拷贝,也就是基于被拷贝的可变对象,建立一个完全一样的拷贝后的对象,二者之间除了长得一模一样,但是相互独立,并不会互相影响。

我们以列表为例,举个单层常规列表的栗子:

# 对于单层常规列表,经过深拷贝操作后,拷贝后得到的对象的任何操作均无法改变原始对象
# Python的原生拷贝操作需要导入copy包,其中的deepcopy()函数表示深拷贝
import copy
m = ["Jack", "Tom", "Brown"]
n = copy.deepcopy(m)

# 判断二者是否相等,结果显而易见
m == n
Out: True

# 判断二者是否具有同一性,答案为否。也就是说,列表m和列表n是存储在不同的地址里。
m is n
Out: False

# 改变m首位的元素,发现n并无变化,说明二者互不影响
m[0] = "Helen"
m
Out: ['Helen', 'Tom', 'Brown']
n
Out: ['Jack', 'Tom', 'Brown']

上述案例说明,我们在对可变对象 m 使用深拷贝的时候,是完全复制了 m 的数据结构 ,并赋值给了 n。这也符合我们直观上的认识。

  1. 浅拷贝

其实既然 Pythoncopy 有深浅之分,那显然浅拷贝必然有不一样的地方。如果只是针对由不可变的对象组成的单层常规列表,浅拷贝和深拷贝并无任何区别。这里我们简单做一下测试:

# 用copy库中的copy方法来表示浅拷贝
import copy
m = ["Jack", "Tom", "Brown"]
n = copy.copy(m)

# 判断浅拷贝前后是否具有同一性,答案是不具备同一性
m is n
Out: False

# 更改m的值,发现n并无任何变化。这里的规律保持和深拷贝一致
m[0] = "Black"
n
Out: ['Jack', 'Tom', 'Brown']

那如果是嵌套列表呢?会有一些不一样的地方。我们创建一个 2层列表,内存用长度为 3的列表表示学生的姓名、身高和体重;外层列表的首位表示班级:

# students列表的长度为3,其中首位为字符串,其他位均为列表
students = ["Class 1", ["Jack", 178, 120], ["Tom", 174, 109]]
students_c = copy.copy(students)

# 查看内嵌的列表是否具备同一性
students[1] is students_c[1]
Out: True

# 尝试更改students中某位学生的信息,通过测试更改后的students和students_c
students[1][1] = 180
students
Out: ['Class 1', ['Jack', 180, 120], ['Tom', 174, 109]]
students_c
Out: ['Class 1', ['Jack', 180, 120], ['Tom', 174, 109]]
## 发现:students_c也跟着改变了,这说明对于嵌套列表里的可变元素(深层次的数据结构),浅拷贝并没有进行拷贝,只是对其进行了引用

# 我们接着尝试更改students中的班级信息
students[0] = "Class 2"
students
Out: ['Class 2', ['Jack', 180, 120], ['Tom', 174, 109]]
students_c
Out: ['Class 1', ['Jack', 180, 120], ['Tom', 174, 109]]
## 发现:students_c没有发生变化,这说明对于嵌套列表里的不可变元素,浅拷贝和深拷贝效果一样

通过上述研究,我们能够得到如下结论:

1)由不可变对象组成的列表,浅拷贝和深拷贝效果一样,拷贝前后互相独立,互不影响;

2)当列表中含有可变元素时,浅拷贝只是建立了一个由该元素指向新列表的引用(指针),当该元素发生变化的时候,拷贝后的对象也会发生变化;

3)深拷贝完全不考虑节约内存,浅拷贝则相对来讲比较节约内存,浅拷贝仅仅是拷贝第一层元素;

  1. 切片与浅拷贝

通常来讲,我们对列表进行复制,切片是一种广泛且方便的操作,那么如果我们更改切片后得到的列表结构,会引起源列表的变化吗?

我们先上结论:切片其实就是对源列表进行部分元素的浅拷贝!

# 我们沿用上面的students列表的数据,通过对students进行切片等一系列微操作
students = ["Class 1", ["Jack", 178, 120], ["Tom", 174, 109]]
students_silce = students[:2]

# 对students的前2项进行切片,并赋值给students_silce;
# 修改students_silce的第二项,修改其中身高值,并比较源列表和切片结果的变化
students_silce[-1][1] = 185
students_silce
Out: ['Class 1', ['Jack', 185, 120]]
students
Out: ['Class 1', ['Jack', 185, 120], ['Tom', 174, 109]]
## 比较发现,切片结果的变化值,也传递给了源列表。说明可变元素的数据结构只是被引用,没有被复制。

# 修改students_silce的第一项,修改班级名,并比较源列表和切片结果的变化
students_silce[0] = "Class 3"
students_silce
Out: ['Class 3', ['Jack', 185, 120]]
students
Out: ['Class 1', ['Jack', 185, 120], ['Tom', 174, 109]]
# 比较发现,切片结果的变化值,没有传递给了源列表。说明对于不可变元素,切片前后互相独立。

## 综合比较,可以发现,切片的效果其实就是浅拷贝!

Python 的浅拷贝和深拷贝是理解Python原理的基础,深入理解二者的区别,对后续进阶、以及理解多维数组很有帮助。

2. Numpy篇

我们前面提到了,Numpy 为了适应大数据的特点,对内存做了优化。所谓的优化,就是以节约内存为前提,尽量在切片过程中减少对内存的开销。

对于 Numpy 来讲,我们主要甄别两个概念,即视图与副本。(注意了,因为多维数组本可以视作嵌套列表,因此嵌套列表的浅拷贝的概念,在这里同样适用。其实多维数组视图的效果,可以理解为嵌套列表的浅拷贝。副本则和深拷贝的概念基本一致)

视图 view 是对数据的引用,通过该引用,可以方便地访问、操作原有数据,但原有数据不会产生拷贝。如果我们对视图进行修改,它会影响到原始数据,因为它们的物理内存在同一位置。

副本是对数据的完整拷贝(Python中深拷贝的概念),如果我们对副本进行修改,它不会影响到原始数据,它们的物理内存不在同一位置。

  • view 视图

创建视图,我们可以通过两种方法:Numpy的切片操作以及调用 view() 函数。

我们先看一下利调用 view() 函数创建视图的案例:

# 视图是新建了一个引用,但是更改视图的维数,并不会引起原始数组的变化
import numpy as np
arr_0 = np.arange(12).reshape(3,4)
view_0 = arr_0.view()
view_0
Out: array([[ 0,  1,  2,  3],
           [ 4,  5,  6,  7],
           [ 8,  9, 10, 11]])

# 从id看,二者并不具备同一性。
id(arr_0) is view_0
Out: False
# 更改视图的元素,则原始数据会产生联动效果
view_0[1,1] = 100
view_01
Out: array([[  0,   1,   2,   3],
           [  4, 100,   6,   7],
           [  8,   9,  10,  11]])

# 更改视图的维度:
# 视图的纬度更改并不会传递到原始数组
view_0.shape = (4,3)
print("arr_0 shape:", arr_0.shape, "view_0 shape:", view_0)
Out: arr_0 shape: (3, 4) view_0 shape: (4, 3)

利用切片创建视图朋友们都很熟悉了,我们来看一下在一维数组上测试的效果:

# 对一维数组切片,并对切片后的结果进行更改,查看是否对原始数组产生影响
arr_1 = np.arange(12)
slice_1 = arr_1[:6]
slice_1[3] = 99
slice_1
Out: array([ 0,  1,  2, 99,  4,  5])
# arr_1的第四个元素发生变成了99。在数组是1维的时候,规律和列表有些不一样,这里要特别注意。
arr_1
Out: array([ 0,  1,  2, 99,  4,  5,  6,  7,  8,  9, 10, 11])
  • 副本

副本也就是深拷贝,相对而言对内存的处理比较粗暴,也比较好理解。建立副本前后,两个变量是完全独立的。

# Numpy建立副本的方法稍有不同。
# 方法一,是利用Numy自带的copy函数;
# 方法二,是利用deepcopy()函数。这里我们重点讲解方法一:
arr_2 = np.array([[1,2,3], [4,5,6]])
copy_2 = arr_2.copy()
copy_2[1,1] = 500
copy_2
Out: array([[  1,   2,   3],
           [  4, 500,   6]])
arr_2
Out: array([[1, 2, 3],
           [4, 5, 6]])
# 比较发现,建立副本后,二者互不影响。符合上面的结论。

3. 总结

本章节讲解的知识点偏理论化一些,但是是朋友们从入门到进阶的过程中,必须要走的一步。如果初次阅读无法掌握,可以带着疑问,在后续的实践中一边实战,一边加深理解。

其实总结起来,主要是要区分直接赋值、浅拷贝和深拷贝这3个概念,其中的难点是理解浅拷贝概念。

浅拷贝在Python原生列表中,需要区分是否是嵌套列表。如果是嵌套列表,那么底层的列表和拷贝后的结果会随着一方的改变而改变。如果是由不可变元素组成的列表,那么浅拷贝与深拷贝并无区别。

浅拷贝在Numpy中则简单一些,无论数组的维数是多少,对视图或切片结果进行元素层面的修改时,操作的效果会反映到原始数组里。

到这里呢,Numpy的主要内容就要告一段落了,下一章,带领大家完成Pandas从入门到精通的过程。

猜你喜欢

转载自blog.csdn.net/qq_33254766/article/details/108362831