Task02学习思维导图

注:为了节约行数,默认import numpy as np已经写在每段代码前,不再重复写入,如果有新的包引入,会在代码头部import。
六、副本与视图
前言
在学习本章之前,我们先回顾一下Python中引用和对象的概念:
Python 中,一切皆对象。每个对象由:标识(identity)、类型(type)、value(值)
组成。
- 标识用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数 id(obj) 可返回对象 obj 的标识。
- 类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的 操作。可以使用 type(obj)获得对象的所属类型。
- 值表示对象所存储的数据的信息。使用 print(obj)可以直接打印出值。
举例说明:
【例六、0-1】python原生的对象和引用
#自定义输出函数printInfo
>>> def printInfo(obj):
>>> print("id:%s type:%s"%(id(obj),type(obj)))
>>> a = 2
>>> b = "datawhale"
>>> print("a的信息",end='')
>>> printInfo(a)
a的信息id:4503882768 type:<class 'int'>
>>> print("b的信息",end='')
>>> printInfo(b)
b的信息id:4571412400 type:<class 'str'>

如上图所示,右边分别是两个不同类型的对象,变量a和b分别引用了这两个对象。在python中,变量也成为:对象的引用。变量存储的就是对象的地址。变量通过地址引用了“对象”。
【例六、0-2】python改变变量的引用
>>> data = [x for x in range(5)]
>>> data2 = [x for x in range(5,10)]
>>> print(data,data2)
[0, 1, 2, 3, 4] [5, 6, 7, 8, 9]
>>> a = data
>>> print(a,id(a),data,id(data))
[0, 1, 2, 3, 4] 4570869056 [0, 1, 2, 3, 4] 4570869056
>>> a[0] = 100
>>> print(a,id(a),data,id(data))
[100, 1, 2, 3, 4] 4570869056 [100, 1, 2, 3, 4] 4570869056
>>> a = data2
>>> print(a,id(a),data,id(data),data2,id(data2))
[5, 6, 7, 8, 9] 4654437248 [100, 1, 2, 3, 4] 4570869056 [5, 6, 7, 8, 9] 4654437248

如上图,我们可以看到这样的过程,开始时,我们可以通过变量a修改data的数据,变量a的引用从data转向data2后,a的id和数据都发生了改变,与data2保持一致。
1.numpy中的变量引用
类比前言中python的引用例子,我们也可以看一下对numpy的数据类型ndarray进行引用会发生什么:
【六、例1】引用并修改ndarray变量
>>> data = np.arange(5)
>>> a = data
>>> print(a,id(a),data,id(data))
>>> print(a is data)
[0 1 2 3 4] 4659785120 [0 1 2 3 4] 4659785120
True
>>> a[0] = 100
>>> print(a,id(a),data,id(data))
>>> print(a is data)
True
a is data也可以用来判断2个变量所引用的对象id是否相同,is也叫做同一性运算符。
我们通过上面的例子可以看出,目前为止,numpy和python原生没有太大区别。
2.视图
视图是数据的一个别称或引用,通过该别称或引用亦便可访问、操作原有数据,但原有数据不会产生拷贝。如果我们对视图进行修改,它会影响到原始数据,物理内存在同一位置。
视图一般发生在:
- numpy 的切片操作返回原数据的视图。
- 调用 ndarray 的 view() 函数产生一个视图。
【六、例2-1】使用numpy切片操作返回视图
#自定义输出函数printInfo
>>> def printInfo(obj):
>>> print(obj,end=" ")
>>> print("id:%s type:%s"%(id(obj),type(obj)))
>>> data = np.arange(5)
>>> print("data:",end='')
>>> printInfo(data)
data:[0 1 2 3 4] id:4656357504 type:<class 'numpy.ndarray'>
>>> a = data[:2]
>>> print("a:",end=' ')
>>> printInfo(a)
a: [0 1] id:4661896176 type:<class 'numpy.ndarray'>
>>> b = data[:-1]
>>> print("b:",end=' ')
>>> printInfo(b)
b: [0 1 2 3] id:4661897776 type:<class 'numpy.ndarray'>
>>> a[0] = 100
>>> b[-1] = 200
>>> print(data)
[100 1 2 200 4]
>>> a.shape = (2,1)
>>> print(a)
>>> print(data)
[[100]
[ 1]]
[100 1 2 200 4]
a和b均由ndarray数据类型切片而来,可以看到,虽然data、a、b三个变量所引用的id不同,但是改变a和b的行为均在data上体现,但是改变a的shape并不会对data的shape产生影响。
【六、例2-2】调用 ndarray 的 view() 函数产生视图
#自定义输出函数printInfo
>>> def printInfo(obj):
>>> print(obj,end=" ")
>>> print("id:%s type:%s"%(id(obj),type(obj)))
>>> data = np.arange(5)
>>> a = data.view()
>>> print("data:",end='')
>>> printInfo(data)
data:[0 1 2 3 4] id:4679609280 type:<class 'numpy.ndarray'>
>>> print("a:",end=' ')
>>> printInfo(a)
a: [0 1 2 3 4] id:4661896976 type:<class 'numpy.ndarray'>
>>> a[0] = 100
>>> print(data)
[100 1 2 3 4]
>>> a.shape = (5,1)
>>> print(a)
>>> print(data)
[[100]
[ 1]
[ 2]
[ 3]
[ 4]]
[100 1 2 3 4]
通过view()方法生成视图,变量a和变量data的id仍不相同,但是改变a仍然可以改变data,另外,用这种视图生成方式改变a的shape,data的shape仍然不会改变。
那么可以把视图理解成半相关,部分相关(数据),部分无关(shape等)。
3、副本
副本是一个数据的完整的拷贝,如果我们对副本进行修改,它不会影响到原始数据,物理内存不在同一位置。
副本一般发生在:
- Python 序列的切片操作,调用deepCopy()函数。
- 调用 ndarray 的 copy() 函数产生一个副本。
【例六、3-1】Python 序列的切片生成副本
>>> a = [x for x in range(5)]
>>> b = a[:-1]
>>> print(a is b)
False
>>> print(a,b)
[0, 1, 2, 3, 4] [0, 1, 2, 3]
>>> b[0] = 100
>>> print(a,b)
[0, 1, 2, 3, 4] [100, 1, 2, 3]
可以看到通过python List的切片操作,生成的b和a的id不同,改变b中的数据也并不会改变a的数据。
【例六、3-2】调用 ndarray 的 copy() 函数生成副本
>>> a = np.arange(5)
>>> b = a.copy()
>>> print(a is b)
False
>>> print(a,b)
[0 1 2 3 4] [0 1 2 3 4]
>>> b[0] = 100
>>> print(a,b)
[0 1 2 3 4] [100 1 2 3 4]
numpy.ndarray.copy() 函数创建一个副本。 对副本数据进行修改,不会影响到原始数据,它们物理内存不在同一位置。
七、索引与切片
前言
ndarray对象的内容可以通过索引或切片来访问和修改,与 Python 中 list 的切片操作一样。
1.整数索引
【七、例1】通过整数索引访问数组
>>> a = np.random.randint(0,10,5)
>>> print(a,a[2])
[2 7 3 6 6] 3
>>> b = np.random.randint(0,10,(5,5))
>>> print(b,b[3][1],b[3,1])
[[3 7 0 7 9]
[1 6 9 3 1]
[2 7 5 0 7]
[6 2 6 1 0]
[5 5 6 8 1]] 2 2
>>> c = np.random.randint(0,10,(3,3,3))
>>> print(c,c[0][0][0],c[0,0,0])
[[[6 1 5]
[9 5 9]
[2 3 5]]
[[1 1 5]
[2 1 5]
[1 5 4]]
[[3 5 9]
[8 9 6]
[7 3 3]]] 6 6
这里用了np.random.randint()方法生成了随机ndarray数组,然后利用整数索引进行访问。
注:[x][y][z] 与 [x,y,z]的效果相同

顾名思义,我们可以把切片理解成一系列切蛋糕的行为,第一个参数是第一刀,第二个参数是最后一刀后面的位置,第三个参数是刀与刀之间的距离,那么整数索引在二维数组就相当于与横纵都仅切1刀,且刀宽为1。
2.切片索引
我们在上一章知道,python切片获得原来数据的副本(深拷贝),而ndarray数据类型切片获得原数据的视图。
ndarray 数组可以基于 0 - n 的下标进行索引,切片对象可以通过内置的 slice 函数,并设置 start, stop 及 step 参数进行,从原数组中切割出一个新数组,也可以通过冒号分隔切片参数 start:stop:step 来进行切片操作。
【七、例2-1】单维度切片
#一维数组
>>> a = np.random.randint(0,10,5)
>>> print(a)
>>> print(a[:])
>>> print(a[:-2])
>>> print(a[1:])
>>> print(a[::2])
[8 6 4 8 6]
[8 6 4 8 6]
[8 6 4]
[6 4 8 6]
[8 4 6]
#二维数组
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[1 7 5 8 3]
[1 7 8 7 3]
[2 1 7 3 7]
[3 3 4 1 4]
[6 4 3 8 6]]
#打印倒数前2行之前的数组(不包括倒数第二行)
>>> print(a[:-2])
[[1 7 5 8 3]
[1 7 8 7 3]
[2 1 7 3 7]]
#步长为2打印整个数组
>>> print(a[::2])
[[1 7 5 8 3]
[2 1 7 3 7]
[6 4 3 8 6]]
#打印倒数前2列之前的数组(不包括倒数第二列)
>>> print(a[...,:-2])
[[1 7 5]
[1 7 8]
[2 1 7]
[3 3 4]
[6 4 3]]
#打印倒数前2行之前的数组(不包括倒数第二行)
>>> print(a[:-2,...])
[[1 7 5 8 3]
[1 7 8 7 3]
[2 1 7 3 7]]
二维数组单维度切片示意图如下:

上面的案例仅以一维、二维数组为例进行单维度切片(在二维数组中要么是行,要么是列),上升到多维数组后,行列就要换成第几维去描述。
我们注意到a[:-2]其实是和a[:-2,...]是等价的,那么就此引出dots:
NumPy 允许使用
...表示足够多的冒号来构建完整的索引列表。
那么在上述案例中,:::其实就和...是等价。对于其他维度的数组,...可以起到一个补全的效果,我们接下来会在多维度切片中进行演示:
【七、例2-2】多维度切片
#二维数组
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[7 7 9 8 0]
[7 9 9 9 9]
[2 1 4 7 7]
[4 9 7 3 8]
[2 1 8 2 8]]
#第2行与第3、4列交汇的数据
>>> print(a[1,2:4])
[9 9]
#第2列与第3、4行交汇的数据
>>> print(a[2:4,1])
[1 9]
#第3、4行与第3、4列交汇的数据
>>> print(a[2:4,2:4])
[[4 7]
[7 3]]
#从第一行开始以2为步长的行与从第一行开始以2为步长的列交汇的数据
>>> print(a[::2,::2])
[[7 9 0]
[2 4 7]
[2 8 8]]
#去掉最后一列
>>> print(a[...,:-1])
[[7 7 9 8]
[7 9 9 9]
[2 1 4 7]
[4 9 7 3]
[2 1 8 2]]
二维数组多维度切片示意图:

复杂版:

在这里比较好奇,如果是一行一列,返回的结果是怎样的呢?
>>> print(a[0,0])
7
是一个整数,切片切多了,忘了[0,0]其实就是整数索引 —_-!
3.整数数组索引
先简单看一个案例:
【七、例3-1】单维度整数数组索引
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[0 3 2 8 0]
[2 6 8 7 9]
[4 0 5 0 5]
[8 5 9 0 0]
[1 9 8 8 5]]
>>> s = [0,2,4]
#取a的第1,3,5行
>>> print(a[s])
[[0 3 2 8 0]
[4 0 5 0 5]
[1 9 8 8 5]]
整数数组解决的是不连续切片且间隔是随机非等差的问题,上面的案例是操作行,我们也可以同时操作行和列:
【七、例3-2】多维度整数数组索引
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[6 2 9 7 2]
[8 5 4 5 9]
[1 4 3 3 9]
[9 8 0 8 8]
[7 1 6 5 2]]
>>> s = [0,2,3]
>>> p = [1,2,4]
>>> q = [1,2]
>>> print(a[s,p])
[2 3 8]
>>> print(a[s,q])
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,)
最后一句代码出现问题,原因是传入的两个index数组的数量必须相等,怎样理解,就是将整数索引重复n遍,那么两个index数组当然要相等。
二维数组多维度整数数组切片示意图:

假设a是一个ndarray的二维数组,p是一个一维list,那么a[p]的结果是一个ndarray二维数组。
若p也是一个ndarray数组呢?请看下面的例子:
【七、例3-3】索引标志为ndarray的整数数组索引(单维度)
#单维度情况
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[5 0 1 7 4]
[3 9 2 5 1]
[3 5 8 3 2]
[5 4 4 6 5]
[5 7 7 5 6]]
>>> p = np.array([[0,2],[2,3]])
>>> print(a[p])
[[[5 0 1 7 4]
[3 5 8 3 2]]
[[3 5 8 3 2]
[5 4 4 6 5]]]
你会发现其实索引标志为ndarray时,其实就是多次整数数组索引,然后再将结果整合到一个ndarray中,在这种单维度的情况下:
目标数组二维+索引标志一维 = 结果数组二维
目标数组二维+索引标志二维 = 结果数组三维
…
显然,结果数组维数 = 目标数组维数 + 索引标志维数 - 1
【七、例3-4】索引标志为ndarray的整数数组索引(多维度)
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[0 9 8 2 4]
[7 8 7 2 0]
[8 7 3 1 5]
[9 8 4 8 2]
[0 9 7 9 4]]
>>> p = np.array([[0,2],[2,3]])
>>> q = np.array([[0,4],[0,4]])
>>> print(a[p,q])
[[0 5]
[8 2]]
其实可以把这种索引标志为ndarray的切片分解成多个子切片行为,即[a[p1,q1]+a[p2+q2]],如下图所示,求解过程可化解为2个普通多维度整数数组索引,如例3-2图所示。

易知,条件允许的情况下:
- 目标数组维数 + 索引标志维数 - 2
- 且多个索引标志维数必须相等,不然会报错
我们再来介绍一下take()函数:
numpy.take(a,indices,axis = None,out = None,mode =‘raise’ )

其中,当axis不是None时,此函数与使用数组索引数组的功能相同,但是若未指定axis时,会默认把目标数组维度打成一维,数据扁平化:
【七、例3-5】ndarray.take()的使用
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[8 4 0 3 2]
[0 0 1 0 8]
[6 7 4 7 2]
[0 7 9 6 2]
[4 0 9 1 9]]
>>> print(np.take(a,[0,1,2],axis=0))
[[8 4 0 3 2]
[0 0 1 0 8]
[6 7 4 7 2]]
>>> print(np.take(a,[0,1,2],axis=1))
[[8 4 0]
[0 0 1]
[6 7 4]
[0 7 9]
[4 0 9]]
>>> print(np.take(a,[0,1,2]))
[8 4 0]
如上述案例所示,当axis=0时,取的是1-3行;当axis=1时,取的是1-3列;当axis未赋值时,数据被扁平化,取的是扁平化后的前三个数组元素。
4.布尔索引
与其称为布尔索引,不如理解成条件索引:
【七、例4-1】控制最小值及类型的布尔索引
#控制最小值
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[5 9 6 6 1]
[0 0 4 0 0]
[0 8 2 5 7]
[8 0 2 0 0]
[3 4 0 1 6]]
>>> b = a > 5
>>> print(b)
[[False True True True False]
[False False False False False]
[False True False False True]
[ True False False False False]
[False False False False True]]
>>> print(a[b])
[9 6 6 8 7 8 6]
#控制类型,找到不是nan的数据
>>> c = np.array([np.nan,9,8,7,np.nan,6,5])
>>> d = np.logical_not(np.isnan(c))
>>> print(d)
[False True True True False True True]
>>> print(c[d])
[9. 8. 7. 6. 5.]
我们其实可以看出,最终带入的就是一个布尔数组b,然后利用a[b]去生成满足条件的数组。
【七、例4-2】布尔索引在matplotlib上的应用
>>> import matplotlib.pyplot as plt
#y = sinx函数图像 x在[0,2pi]之间,最小刻度为pi/20
>>> x = np.linspace(0, 2 * np.pi, 50)
>>> y = np.sin(x)
>>> print(len(x))
>>> plt.plot(x, y)
#mask1
#mask1 = y >= 0
>>> mask1 = np.logical_and(y >= 0, x >= 0)
>>> plt.plot(x[mask1],y[mask1],'ro')
#mask2
>>> mask2 = np.logical_and(y <= 0, x >= 3*np.pi/2 )
>>> plt.plot(x[mask2],y[mask2],'yo')

解释一下np.logical_and的含义,这个需要同时满足参数条件,比如 np.logical_and(y >= 0, x >= 0)代表x,y需要同时大于0。
可以把mask理解成一个范式,把它放入x,y中会根据范式规则返回需要的数据。
八、数组迭代
前言
除了for循环,Numpy 还提供另外一种更为优雅的遍历方法。
apply_along_axis(func1d, axis, arr)Apply a function to 1-D slices along the given axis.
第一个参数是要执行的函数,第二个参数是遍历的维度,第三个参数是要遍历的数据
1.采用系统函数迭代
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[6 8 7 0 5]
[3 4 4 3 7]
[6 0 4 2 4]
[3 9 8 4 8]
[8 9 3 5 7]]
>>> b = np.apply_along_axis(np.sum, 0, x)
>>> print(b)
[25 12 28 12 28]
>>> b = np.apply_along_axis(np.sum, 1, x)
>>> print(b)
[28 19 16 24 18]
这里解释一下axis = 0的含义,以二维数组距离,当axis = 0时,是指遍历的方向是行,也就是说列是固定的,比如第一列6+3+6+3+8=25,就是结果数组的第一个数,以此类推。axis = 1的含义是,遍历的方向是列,示意图如下:

2.采用自定义函数迭代
也就是更改第一个参数,如下:
>>> def xiao_func(x):
>>> return (x[0] + x[3])
>>> b = np.apply_along_axis(xiao_func, 0, a)
>>> print(b)
[14 8 10 8 12]
练习
1.交换数组arr中的列1和列2
#交换1,2列
>>> arr = np.arange(9).reshape(3, 3)
>>> print(arr,id(arr))
[[0 1 2]
[3 4 5]
[6 7 8]] 4711093952
>>> a = arr.copy()
>>> arr[...,0] = a[...,1]
>>> arr[...,1] = a[...,0]
>>> print(arr,id(arr))
[[1 0 2]
[4 3 5]
[7 6 8]] 4711093952
2.交换数组arr中的行1和行2
#交换1,2行
>>> arr = np.arange(9).reshape(3, 3)
>>> print(arr,id(arr))
[[0 1 2]
[3 4 5]
[6 7 8]] 4711349344
>>> a = arr.copy()
>>> arr[0] = a[1]
>>> arr[1] = a[0]
>>> print(arr,id(arr))
[[3 4 5]
[0 1 2]
[6 7 8]] 4711349344
3.反转二维数组arr的行
#反转行
>>> arr = np.arange(9).reshape(3, 3)
>>> print(arr,id(arr))
[[0 1 2]
[3 4 5]
[6 7 8]] 4713855696
>>> a = arr.copy()
>>> arr[:] = a[::-1]
>>> print(arr,id(arr))
[[6 7 8]
[3 4 5]
[0 1 2]] 4713855696
注意,这里如果有arr = a[::-1]是会改变变量arr的引用,但是原id的数据不会产生变化:

4.反转二维数组arr的列
#反转列
>>> arr = np.arange(9).reshape(3, 3)
>>> print(arr,id(arr))
[[0 1 2]
[3 4 5]
[6 7 8]] 4712885584
>>> a = arr.copy()
>>> arr[...,:] = a[...,::-1]
>>> print(arr,id(arr))
[[2 1 0]
[5 4 3]
[8 7 6]] 4712885584
为什么我要打印id,因为题目是交换数组arr中的行和列以及反转,如果采用x = arr[:, [1 , 0 , 2]]的方法,确实能得到交换后的数组,但是其实并没有改变arr本身,而是新生成了一个ndarray。

本文详细介绍了numpy中数组的副本、视图概念,通过实例展示了切片操作如何产生视图以及如何通过整数、切片、布尔索引访问数组。同时,讲解了数组的迭代方法apply_along_axis,以及如何交换和反转数组的行和列。
798

被折叠的 条评论
为什么被折叠?



