数组索引指的是通过索引信息来获取数组中元素的一种方法,通常在[]中给出目标元素的索引信息,在NumPy中,索引规则是非常强大和灵活的。

单元素索引

对一维数组来说,索引的规则和原生Python中的索引规则相同,以0作为起始位置,如果索引值为负数,则从数组末尾开始查询。

  1. import numpy as np
  2. v = np.arange(4) # array([0, 1, 2, 3])
  3. print(v[0], v[-1]) # 0 3

list等序列类型不同,NumPy数组可以将每个维度的索引值写在一起对多维数组进行索引操作,而不是将各个维度的索引值写在多个[]中。如果提供的索引值数量小于数组的维度,则会生成一个子数组。

  1. import numpy as np
  2. v = np.array([[1, 2], [3, 4]])
  3. x_0 = v[0, 0] # 1
  4. x_1 = v[0, 1] # 2
  5. sub_v = v[1] # array([3, 4])

对上面的数组v,现在有两种方式可以索引到其中的一个元素,一种是直接提供各维度的索引信息:v[0, 0],另外一种是通过子数组二次索引v[0][0]。这两种索引方式中,第二种效率会差一点,因为中间生成了一次子数组。通过下面这个例子可以对比一下:

  1. import numpy as np
  2. import time
  3. v = np.ones((2, 3, 4, 2), dtype=np.int8) # 生成一个四维数组,维度越高,差别越明显
  4. sum = 0
  5. start = time.clock()
  6. for i in range(1000000):
  7. sum += v[1][1][1][1] # 加法运算,消耗下时间
  8. stop = time.clock()
  9. for i in range(1000000):
  10. sum -= v[1, 1, 1, 1] # 和上面的差别主要来自索引取值
  11. end = time.clock()
  12. print("Time for v[1][1][1][1]: %.2f" % (stop - start))
  13. print("Time for v[1, 1, 1, 1]: %.2f" % (end - stop))
  14. # Time for v[1][1][1][1]: 0.73s
  15. # Time for v[1, 1, 1, 1]: 0.34s

多元素索引(切片)

这里和原生Python中对序列类型进行切片的规则是一样的,可以从整个数组中按规则取出一部分构成新的数组。

  1. import numpy as np
  2. v = np.arange(6)
  3. print(repr(v[1:3])) # array([1, 2])
  4. print(repr(v[::2])) # array([0, 2, 4])
  5. v = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9])
  6. print(repr(v[:2, :2]))
  7. # array([[1, 2],
  8. # [4, 5]])

但是,对NumPy数组的切片,并不仅仅是生成了对应数据的一份拷贝,而是建立了和原始数据相关联的视图。看下面这个例子:

  1. import numpy as np
  2. v = np.arange(6)
  3. t = v[::]
  4. print(id(v), id(t)) # 140670553636016 140670346333120

[::]是在Python中对序列类型进行拷贝的一种方法,生成原始数据的一个备份。结合上面的操作,我们看到,v,t两个数组的id值并不相同,这意味这它们在内存中已经不是同一个对象,但并不能认为他们之间毫无关联了,现在对vt中的元素进行修改:

  1. v[0], t[-1] = 666, 999
  2. print(repr(v)) # array([666, 1, 2, 3, 4, 999])
  3. print(repr(t)) # array([666, 1, 2, 3, 4, 999])

可以看到,不管修改哪个数组,都会对另一个造成影响。如果确实想得到拷贝形式的结果,z在执行切片的操作后,还要显示地调用copy()方法:

  1. v = np.arange(3)
  2. t = v[::].copy()
  3. t[0] = 666
  4. print(repr(v)) # array([0, 1, 2])
  5. print(repr(t)) # array([666, 1, 2])

数组切片之后可以直接用常数赋值,这一点和Python的序列类型有些不同。

  1. """
  2. 被切出来的元素都会被赋值
  3. """
  4. import numpy as np
  5. a = np.arange(6) # array([0, 1, 2, 3, 4, 5])
  6. a[1:3] = 666
  7. print(a) # [ 0 666 666 666 4 5]
  8. b = a.reshape(2, 3)
  9. b[:, :2] = 666
  10. """
  11. [:] == [::] == [0:2] 表示第一维度维度元素全选
  12. """
  13. print(b)
  14. # [[666 666 2]
  15. # [666 666 5]]
  16. li = [0, 1, 2, 3, 4]
  17. li[1:3] = 666 # TypeError: can only assign an iterable
  18. li[1:3] = [666] # [0, 666, 3, 4]

索引数组

可以用一个数组的索引值构成一个新的数组(索引数组)[也可以是列表],用这个索引数组来对原数组进行索引操作,返回每个索引值对应元素构成的新数组,听起来有点绕……直接看代码把:

  1. import numpy as np
  2. v = np.arange(1, 10)
  3. index_v = np.array([0, 0, 3, 3, -1, -2]) # 由索引值构成的新数组
  4. t = v[index_v] # 用索引数组进行索引操作
  5. """
  6. v[0] ---> 1 v[3] ---> 4
  7. v[-1] ---> 9 v[-2] ---> 8
  8. """
  9. print(repr(t)) # array([1, 1, 4, 4, 9, 8])
  10. mod_index = [1, 2, 3]
  11. v[mod_index] = 666
  12. print(repr(v)) # array([ 1, 666, 666, 666, 5, 6, 7, 8, 9])

更好的理解应该是对应元素去取代索引数组中的索引值,从而产生新的数组。

  1. index_v = np.array([[0, 0], [3, 3], [-1, -2]])
  2. t = v[index_v]
  3. print(repr(t))
  4. # array([[1, 1],
  5. # [4, 4],
  6. # [9, 8]])

对于多维数组,从形式上来看,不是特别容易理解:要将不同维度上的索引值分别构成数组。

  1. v = np.array([
  2. [1, 2, 3, 4],
  3. [5, 6, 7, 8],
  4. [9, 10, 11, 12]
  5. ])
  6. index_x, index_y = [0, 1, 2], [2, 1, 0]
  7. t = v[index_x, index_y]
  8. """
  9. v[0, 2] ---> 3 v[1, 1] ---> 6
  10. v[2, 0] ---> 9
  11. """
  12. print(repr(t)) # array([3, 6, 9])
  13. """
  14. 下面这么写应该比较容易理解,返回数组的「尺寸」是和索引数组「尺寸」对应的
  15. """
  16. index_x = [
  17. [0, 1],
  18. [1, 2]
  19. ]
  20. index_y = [
  21. [0, 1],
  22. [1, 2]
  23. ]
  24. t = v[index_x, index_y] # 这其实就是单元素索引的写法 array[x, y], array[x, y, z]...
  25. """
  26. 便于理解:将两个维度的索引值进行叠加,可以得到高维的直接索引值
  27. [
  28. [(0, 0), (1, 1)],
  29. [(1, 1), (2, 2)]
  30. ]
  31. """
  32. print(repr(t))
  33. # array([[ 1, 6],
  34. # [ 6, 11]])

掩码索引

掩码索引(boolean index),是由boolean类型元素构成的数组,用这样的数组,可以对原数组进行数据筛选工作。

  1. import numpy as np
  2. v = np.array([[1, 2, 3, 4],
  3. [2, 4, 6, 8],
  4. [3, 6, 9, 12],
  5. [999, 8, 12, 16]])
  6. mask = v > 5 # 生成掩码数组
  7. print(repr(mask))
  8. # array([[False, False, False, False],
  9. # [False, False, True, True],
  10. # [False, True, True, True],
  11. # [ True, True, True, True]])
  12. t = v[mask] # 通过掩码筛选出所有符合条件的元素
  13. print(repr(t)) # array([ 6, 8, 6, 9, 12, 999, 8, 12, 16])
  14. """
  15. 掩码数组维度和数组维度相同,筛选出来的是元素
  16. 掩码数组维度比数组维度小,筛选出来的是子数组
  17. """
  18. sub_mask = [False, True, False, False] # 一维,表示保留第二个元素
  19. sub_v = v[sub_mask] # array([[2, 4, 6, 8]]) # 子数组[2, 4, 6, 8]是v的第二个元素
  20. """
  21. 将数组中大于 3 的元素全部置零
  22. """
  23. v = np.array([[0, 1, 2], [2, 3, 4], [3, 4, 5]])
  24. mask = v > 3
  25. v[mask] = 0
  26. print(v)
  27. # [[0 1 2]
  28. # [2 3 0]
  29. # [3 0 0]]