3.数组的切片

根据下标,可以得到数组中的某个或者某几个元素,这是从数组中得到部分元素的一种方法。此外,还有另外一种被称为“切片”的方式,从这个名称和以前学习 Python 的经验可知,切片用来设定某个范围,根据这个范围从原数组中得到部分元素。

还是从简单的一维数组开始讲解。

In [43]: a = np.arange(10, 20)
a
Out[43]: array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
In [44]: b = a[2: 8]
b
Out[44]: array([12, 13, 14, 15, 16, 17])
In[44]的操作跟以往熟知的 Python 列表差不多,也遵守着“前包括、后不包括”的原则。
但是,要注意下面的新情况。
In [45]: b[0] = 12000
b
Out[45]: array([12000, 13, 14, 15, 16, 17])
In [46]: a
Out[46]: array([10, 11, 12000, 13, 14, 15, 16, 17, 18, 19])

In[45]将新得到的数组 b 中的第一个元素的值进行了修改,结果见 Out[45],同时原数组 a中的元素也随之修改,见 Out[46]的结果。

之所以如此,是因为通过切片得到的数组和原数组共享了一个内存空间,通常称为公用同一个视图,当其中一个进行修改后,另一个也随之修改,这种特点与前面通过下标得到某些元素(尽管有的也生成数组)是不同的。

对于一维数组切片的其他方面,完全可以套用 Python 中列表的知识,比如下面所演示的。

In [47]: a[: : 2]
Out[47]: array([ 10, 12000, 14, 16, 18])
In [48]: a[3:]
Out[48]: array([13, 14, 15, 16, 17, 18, 19])
In [49]: a[: : ‐1]
Out[49]: array([ 19, 18, 17, 16, 15, 14, 13, 12000, 11, 10])
In [50]: a[: 6 : 3]
Out[50]: array([10, 13])

对于二维或者更多维度的数组进行切片,也是在一维数组的基础上进行的,只不过分别在每个轴方向上实施,最终获得重叠区域。为了能够使观察效果明显,先创建一个“别有用意”的二维数组。
 

In [51]: b = np.arange(0, 60, 10).reshape(‐1, 1) + np.arange(0, 6)
b
Out[51]: array([[ 0, 1, 2, 3, 4, 5],
[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45],
[50, 51, 52, 53, 54, 55]])

读者不用深究对 In[51]操作的理解,只要知道那样做能获得数组 b 即可,笔者会在后面讲解的。

下面就开始从刚刚创建的二维数组 b 上获得切片。毫无疑问,此数组有 0 轴和 1 轴两个方向,那么在每个轴上都可以看作是一维的(这是重要的分解思想,复杂事物都是由简单元素构成的,所以研究复杂事物的一个方法就是分解法)。

In [52]: b[1: 4]
Out[52]: array([[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35]])
In [53]: b[1 : 4, 2 : 5]
Out[53]: array([[12, 13, 14],
[22, 23, 24],
[32, 33, 34]])

b[1: 4]是在 0 轴方向上切片,得到了按照 0 轴方向上元素为单元的切片后的数组。

b[1 : 4, 2 : 5]是先在 0 轴方向上“切出一片”,然后在 1 轴方向上按照[2 : 5]的要求“切出”,最终得到 Out[53]所示的“那一片”,如图 1-2-5 所示。

请读者认真观察图 1-2-5,体会切片的规则。

提醒读者注意,切片得到的数组跟原数组公用一个视图。这个很重要,请参考前述解释。

下面再用“代码+图示”的方式,展示一些常见的切片操作,读者可以根据下述示例,依次进行测试,并对照(依然以二维数组为例)结果。

In [54]: b[0]
Out[54]: array([0, 1, 2, 3, 4, 5])
In [55]: b[1, :]
Out[55]: array([10, 11, 12, 13, 14, 15])
In[54]和 In[55]数组的切片示意图如图 1-2-6 所示。

In [56]: b[: , 2]
Out[56]: array([ 2, 12, 22, 32, 42, 52])
In [57]: b[0 : 2, 0 : 2]
Out[57]: array([[ 0, 1],
[10, 11]])
In[56]和 In[57]数组的切片示意图如图 1-2-7 所示。

In [58]: b[: : 2, : : 2]
Out[58]: array([[ 0, 2, 4],
[20, 22, 24],
[40, 42, 44]])
In [59]: b[: 3 , [0, 3]]
Out[59]: array([[ 0, 3],
[10, 13],
[20, 23]])
In[58]和 In[59]数组的切片示意图如图 1-2-8 所示。

从上述切片操作的示例中,不难总结出关于切片的两种理解方式。
理解一:先根据 0 轴的要求进行切片,然后将得到的结果根据 1 轴的要求再切片。
理解二:对原数组按照 0 轴和 1 轴两种要求进行切片,将两者的交集作为最终结果。跟老齐学 
以上两种理解方式殊途同归。不过,对于更高维度的数组切片,按照“理解一”可能更容易一些。
现在我们已经能够通过下标或者切片获得数组中的一部分,这种针对数组的操作,是从轴
和索引的角度完成的。此外,还有另外一种针对数组的操作,是通过属性和函数实现的,请看
下节。

1.3 针对数组的操作

对于数组,除根据索引进行上节所述各种操作外,还有很多方法供我们使用,实现更加多
样化的操作,例如本节将要介绍的变形、组合和分割等。
1.数组变形
所谓数组变形,就是将一个已有数组按照要求改变其形状后新生成一个数组,新数组的所
有元素都来自于原数组,并且元素数量也保持不变,变的只有形状。

In [1]: import numpy as np
np.arange(10)
Out[1]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [2]: np.arange(10).reshape((2, 5))
Out[2]: array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])

In[2]中使用的 reshape()是数组对象的方法。 reshape()传入的参数就是新数组的形状。此外, np 也有这个方法,即 np.reshape(),先看一下这个方法的完整表述。

np.reshape(a, newshape, order='C')

其中, a 是原数组, newshape 是接收用元组表达的新数组的形状(一维数组可以是整数)。

在数组中, 有很多数组对象的方法对应于 NumPy 类的方法, 比如这里所说的 reshape()方法,可以是某个具体的数组对象的方法(表示为 a.reshape()),也可以是 NumPy 类的方法(表示为np.reshape())。

In [3]: a = np.arange(10)
b = np.reshape(a, (2,5))
In [4]: b
Out[4]: array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
In [5]: a
Out[5]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [6]: a.shape
Out[6]: (10,)
In [7]: b.shape
Out[7]: (2, 5)

a 是一个一维数组(In[3]),然后用 reshape()方法对数组 a 变形,得到新数组 b, b 相对 a是一个新数组,两者不公用同一个视图,但是两个数组的元素数量和内容一样。

In [8]: c = np.reshape(b, (‐1, 1))
c
Out[8]: array([[0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9]])

In[8]中提供的新数组形状有点奇怪,按照以前的经验, (-1, 1)不是数组形状,不可能存在-1 个元素。的确如此,在这个表示形状的数组中, 1 轴上的数字没有错误,告诉 np.reshape()新数组 1 轴上有 1 个元素;但是 0 轴上,如果说有“-1”个元素,显然这样的“元素个数”是不存在的,这时候 NumPy 会自动推断 0 轴上应该有的元素个数,于是就得到了数组 c。当然,写其他不可能存在的数也可以,如下所示。

In [9]: c = np.reshape(b, (‐2, 1))

In[8]和 In[9]的操作结果是一样的。

请读者注意比较下面两种操作中参数和结果的差异——两个结果貌似相同,实则有差异。

In [10]: np.reshape(b, (10,))
Out[10]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [11]: np.reshape(b, (1, 10))
Out[11]: array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
除使用 reshape()进行变形外,还可以使用数组的 shape 属性进行变形。
In [12]: b
Out[12]: array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
In [13]: b.shape
Out[13]: (2, 5)
In [14]: b.shape = (1, 10)
b
Out[14]: array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
In [15]: b.shape = (10, 1)
b
Out[15]: array([[0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9]])

只是这样变形之后,改变了原有数组的形状,并不是新生成一个数组,所以要慎用。

对于数组对象,除用 np.ndarray.reshape()方法让数组变形外,还有一个方法可以将多维度的数组变成一个一维数组。

In [16]: p = b.flatten()

p

Out[16]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

顾名思义, np.ndarray.flatten()的作用就是将数组“扁平化”——变成一维数组,新的一维数组的内容完全复制了原数组内容,并且相对原数组不在同一个视图。

与 np.ndarray.flatten()等效的方法还有 np.reval()或 np.ndarray.ravel(), 它们也能将数组转换为一维,只不过要注意它们的具体特点。

In [17]: ne = np.ravel(b) #或者 b.ravel()
ne
Out[17]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [18]: ne[1] = 111
ne
Out[18]: array([ 0, 111, 2, 3, 4, 5, 6, 7, 8, 9])
In [19]: b
Out[19]: array([[ 0],
[111],
[ 2],
[ 3],
[ 4],
[ 5],
[ 6],
[ 7],
[ 8],
[ 9]])

认真观察上述操作,不难得出一个结论, np.ravel()或 np.ndarray.ravel()对数组进行变形后得到的新数组,与原数组公用同一个视图。

使用上述方法已经能够实现数组的各种变形了。但 NumPy 觉得还不够,还允许我们使用下标操作完成变形。

In [20]: data = np.arange(0, 5)
c = data[:, np.newaxis]
c
Out[20]: array([[0],
[1],
[2],
[3],
[4]])

In[20]的操作本质是什么?

请读者联想前面讲解的切片知识, data[:, np.newaxis]本质是对数组 data 进行切片, [ ]中逗号前面的部分,表示 0 轴方向上的所有元素,也就是 data 的所有元素;而逗号后面的部分说明了 1 轴方向上的元素在设置取值范围的时候,使用了 np.newaxis,这是什么?欲知晓,看文档。

In [21]: np.newaxis? Type: NoneType String form: None Docstring: <no docstring>

原来 np.newaxis 就是 None, 真的是这样吗?如果是这样, 那么 In[20]中可以用 data[:, None]吗?有想法就要验证。

In [22]: data[:, None]
Out[22]: array([[0],
[1],
[2],
[3],
[4]])

实践证实了猜想,原来在做切片的时候,可以将某个轴设置为 np.newaxis 即 None,而这样做的结果实现了数组的一种变形。

再根据切片部分所习得的知识思考一个问题,通过上述方法变形得到的新数组相对于原数组,是在同一个视图吗?能不能检验你的结论?答案是“在同一个视图”,原因请参考前面关于切片的讲解。

In[20]把 data 数组由一维变成了二维,是将 1 轴设置为 np.newaxis。如果把 0 轴设置为np.newaxis,会有什么效果呢?

In [23]: data.ndim
Out[23]: 1
In [24]: c.ndim
Out[24]: 2
In [25]: d = data[np.newaxis, :]
d
Out[25]: array([[0, 1, 2, 3, 4]])
In [26]: d.ndim
Out[26]: 2
In [27]: data.shape, d.shape
Out[27]: ((5,), (1, 5))
针对这种在切片中使用 np.newaxis 实现变形的方法, NumPy 还提供了一个替代函数——
np.expand_dims()。
In [28]: data2 = np.expand_dims(data, axis=0)
data2
Out[28]: array([[0, 1, 2, 3, 4]])
In [29]: data2.shape
Out[29]: (1, 5)
In [30]: data3 = np.expand_dims(data, axis=1)
data3
Out[30]: array([[0],
[1],
[2],
[3],
[4]])
In [31]: data3.shape
Out[31]: (5, 1)

np.expand_dims()的作用也是根据轴来调整数组形状。

使用数组对象或者 NumPy 的方法,以及用特殊的切片方式实现对数组的变形,这只是针对数组的一种操作,下面要说明的是数组的“分分合合、恩怨情仇”。

2.组合与分割

在 NumPy 中,实现数组与数组的组合,以及针对一个数组进行分割,有几个非常好用的方法。这些方法不仅丰富了针对数组的操作方式,更为解决复杂的实际问题提供了多种可选工具。这就好比行走江湖,不能只会“神行百变”,韦小宝擅长的还有各种其他功夫,在面对不同的实际场景时才能随机应变。

( 1)水平组合

实现水平组合功能的函数形式是 np.hstack(tup),其中参数 tup 是一个元组,包含即将被组合在一起的几个数组。下面的示例假设是二维数组,要求其 0 轴方向的形状一样,而 1 轴方向的形状可以不同。

In [32]: a = np.arange(9).reshape(3, 3)
b = np.arange(12).reshape(3, 4)
c = np.arange(15).reshape(3, 5)
In [33]: np.hstack((a, b))
Out[33]: array([[ 0, 1, 2, 0, 1, 2, 3],
[ 3, 4, 5, 4, 5, 6, 7],
[ 6, 7, 8, 8, 9, 10, 11]])
In [34]: np.hstack((a, b, c))
Out[34]: array([[ 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4],
[ 3, 4, 5, 4, 5, 6, 7, 5, 6, 7, 8, 9],
[ 6, 7, 8, 8, 9, 10, 11, 10, 11, 12, 13, 14]])

此外,还有实现水平组合的另外两种方法,一个是 np.stack(),另一个是 np.concatenate(),这两个函数的使用方法雷同,能够按照任何方向实现数组的组合。但是,一切都要尝试才能获得结果。
 

In [35]: np.stack((a, b), axis=1)
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
ValueError Traceback (most recent call last)
· 41·
<ipython‐input‐98‐aa3fe05e7512> in <module>()
‐‐‐‐> 1 np.stack((a,b), axis=1)
/usr/local/lib/python3.5/dist‐packages/numpy/core/shape_base.py in stack(arrays, axis)
352 shapes = set(arr.shape for arr in arrays)
353 if len(shapes) != 1:
‐‐> 354 raise ValueError('all input arrays must have the same shape')
355
356 result_ndim = arrays[0].ndim + 1
ValueError: all input arrays must have the same shape
In [36]: np.concatenate((a, b), axis=1)
Out[36]: array([[ 0, 1, 2, 0, 1, 2, 3],
[ 3, 4, 5, 4, 5, 6, 7],
[ 6, 7, 8, 8, 9, 10, 11]])

原来, np.stack()所要求的数组必须“Each array must have the same shape”(引自文档),而np.concatenate()对被组合的数组的要求是“The arrays must have the same shape, except in the dimension corresponding to `axis` (the first, by default)”,所以, np.stack()必须按下面这样操作。
 

In [37]: m = a * 3
a.shape == m.shape
Out[37]: True
In [38]: np.stack((a, m), axis=1)
Out[38]: array([[[ 0, 1, 2],
[ 0, 3, 6]],
[[ 3, 4, 5],
[ 9, 12, 15]],
[[ 6, 7, 8],
[18, 21, 24]]])

从组合的效果来看, np.stack()也迥异于 np.concatenate()。只有 In[36]的 np.concatenate((a,b), axis=1)与 np.hstack((a,b))效果等同。

( 2)垂直组合

前面的水平组合是沿着 1 轴组合(对于二维数组而言如此,对于多维数组也是如此),那么这里的垂直组合,则是沿着与 1 轴垂直的方向(0 轴)组合。之所以用二维数组举例,是因为这种类型的数组较常用,更多维的数组不如二维数组使用广泛。

实现垂直组合的专有函数是 np.vstack(),有了前述经验,操作此方法应该简单了。

In [39]: np.vstack((a, b))
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
ValueError Traceback (most recent call last)
<ipython‐input‐118‐08bbbce58d8b> in <module>()
‐‐‐‐> 1 np.vstack((a,b))
/usr/local/lib/python3.5/dist‐packages/numpy/core/shape_base.py in vstack(tup)
235
236 """
‐‐> 237 return _nx.concatenate([atleast_2d(_m) for _m in tup], 0)
238
239 def hstack(tup):
ValueError: all the input array dimensions except for the concatenation axis must match
exactly
错误再次出现,注意看提示信息。不要忘记 a、 b 两个数组的形状。
In [40]: a.shape, b.shape
Out[40]: ((3, 3), (3, 4))
np.vstack()要求被组合的数组在 1 轴方向上的形状相同。
In [41]: b2 = b.T
b2.shape
Out[42]: (4, 3)
In [43]: np.vstack((a, b2))
Out[43]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])

b.T 是对原数组进行转置,即将 0 轴和 1 轴互换,得到的新数组在 1 轴方向上与数组 a 形状相同,之后顺利实现了 In[43]的操作。

np.concatenate()在传入轴的参数之后,也能实现垂直组合,与上述操作等效。

In [44]: np.concatenate((a, b2), axis=0)
Out[44]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐