6.6. Индексация и срезы

К ndarray обращаются четырьмя способами: одиночные индексы, срезы, булевы маски и формы присваивания каждого из них.

6.6.1. Одиночные элементы

Индексация в квадратных скобках возвращает значение в заданной позиции:

a = np.arange(10, dtype=np.uint8)
print(a[0], a[-1])      # 0 9
print(a[1], a[-2])      # 1 8

Отрицательные индексы отсчитываются с конца, так же как для списка Python list. Индекс вне диапазона вызывает IndexError.

Для массивов более высокого ранга каждая ось принимает индекс. Индексы помещаются в один набор скобок, разделённые запятыми:

m = np.arange(9, dtype=np.uint8).reshape((3, 3))
print(m[1, 1])          # 4
print(m[2, 0])          # 6

Когда индексов передано меньше, чем осей, неиндексированные оси остаются нетронутыми. Результат – представление пониженного ранга источника:

print(m[0])             # the first row, as a 1-D view of m

6.6.2. Срезы

Срез start:stop:step возвращает представление массива. Представление разделяет лежащий в основе буфер данных с источником; запись через представление записывает в источник:

a = np.arange(10, dtype=np.uint8)
v = a[::2]              # array([0, 2, 4, 6, 8], dtype=uint8)
v[0] = 99
print(a)
# array([99, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

Когда нужен независимый буфер, copy() создаёт его явно.

Срезы естественным образом распространяются на более высокие размерности. Каждая ось принимает свой собственный срез:

m = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]], dtype=np.uint8)

m[0]            # first row
m[0, :2]        # first two elements of row 0
m[:, 0]         # column 0 (still 2-D in ulab)
m[-1]           # last row
m[::2, ::2]     # every other row, every other column

Смешивание целого числа (одиночный индекс, отбрасывает ось) и среза (сохраняет ось) допускается и является обычным способом записи доступа к одной строке или одному столбцу.

6.6.3. Булевы маски

Булев массив той же формы, что и источник, выбирает элементы, где маска равна True. Булева индексация в настоящее время работает с одномерными массивами; входные данные более высокого ранга вызывают NotImplementedError:

a = np.arange(9, dtype=np.float)
mask = a < 5
print(a[mask])

Вывод:

array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)

Маска – это обычный bool ndarray, поэтому работает любое выражение, дающее такую:

b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
a = np.arange(9, dtype=np.uint8)
print(a[a * a > np.sin(b) * 100.0])

Булева индексация возвращает копию. Выбранные элементы лежат в тех позициях, где маска равна True, – а не через регулярный шаг по источнику, – поэтому нет дескриптора, который представление могло бы использовать для обращения к ним, и результат материализуется в свой собственный буфер.

6.6.4. Индексация массивом целых чисел

Передача списка или массива индексов в скобках выбирает эти элементы за один шаг:

a = np.array([10, 20, 30, 40, 50], dtype=np.uint8)
a[[0, 2, 4]]
# array([10, 30, 50], dtype=uint8)

Результат – копия; выбранные элементы больше не разделяют хранилище с источником. Та же форма работает в левой части присваивания:

a[[0, 2, 4]] = 0
# array([0, 20, 0, 40, 0], dtype=uint8)

take() (рассмотрена на Выборка и перестановка) – функциональная форма той же операции, принимающая ключевое слово out= для использования без выделения памяти в потоковом цикле.

6.6.5. Присваивание срезу

Срезы и маски появляются в левой части присваивания так же, как и в правой. Правая часть может быть скаляром, другим массивом или представлением:

m = np.zeros((3, 3), dtype=np.uint8)
m[0]      = 1            # whole row 0 set to 1
m[:, 2]   = 3            # whole column 2 set to 3
m[1, 1:3] = [7, 8]       # row 1, columns 1 and 2

Булевы маски слева заменяют элементы, удовлетворяющие условию:

a = np.arange(9, dtype=np.uint8)
a[a < 3] = 99
# array([99, 99, 99, 3, 4, 5, 6, 7, 8], dtype=uint8)

a = np.arange(9, dtype=np.uint8)
b = np.array(range(9)) + 12
a[b < 15] = b[b < 15]
# array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)

6.6.6. Почему присваивание срезу важно на камере

Присваивание срезу записывает через уже существующий массив. Новый массив не выделяется. В этом и есть разница между:

out = a + b              # makes a new array the size of a
out = out * 2            # makes another new array

и:

out[:] = a               # writes into the existing out
out   += b               # in place
out   *= 2               # in place

Первая версия запрашивает у камеры RAM на два свежих массива; вторая версия не запрашивает ничего. На микроконтроллере с ограниченной RAM эта разница часто оказывается разницей между скриптом, который работает комфортно, и тем, у которого заканчивается память.

Производительность подробно рассматривает этот шаблон. Важное правило на данный момент в том, что присваивание срезу, арифметические операторы на месте (+=, *=, …) и ключевое слово out= у универсальных функций – три инструмента, делающие возможными обновления без выделения памяти.