Индексирование
Индексирование массивов относится к любому использованию квадратных скобок [] для индексирования значений массива. Существует множество вариантов индексирования, которые придают индексированию numpy большую мощность, но с силой приходит и некоторая сложность, а также потенциальная путаница. Этот раздел представляет собой лишь обзор различных вариантов и проблем, связанных с индексированием. Помимо индексирования отдельных элементов, подробности по большинству этих вариантов можно найти в соответствующих разделах.
Присваивание против ссылки
Большинство следующих примеров демонстрируют использование индексирования при обращении к данным в массиве. Примеры работают так же хорошо, когда присваивают значение массиву. Подробности о том, как работают присваивания, см. в конце раздела.
Индексирование одного элемента
Индексирование одного элемента для одномерного массива соответствует ожиданиям. Оно работает точно так же, как и для других стандартных последовательностей Python. Оно основано на нулевом индексе и допускает отрицательные индексы для индексирования с конца массива.
>>> x = np.arange(10) >>> x[2] 2 >>> x[-2] 8
В отличие от списков и кортежей, массивы numpy поддерживают многомерное индексирование для многомерных массивов. Это означает, что нет необходимости разделять индексы каждой размерности на отдельные наборы квадратных скобок.
>>> x.shape = (2,5) # now x is 2-dimensional >>> x[1,3] 8 >>> x[1,-1] 9
Обратите внимание, что если индексировать многомерный массив количеством индексов меньше, чем размерностей, то получается подмассив меньшей размерности. Например:
>>> x[0] array([0, 1, 2, 3, 4])
То есть каждый указанный индекс выбирает массив, соответствующий остальным выбранным размерностям. В приведенном выше примере выбор 0 означает, что оставшаяся размерность длиной 5 остается неуказанной, и возвращается массив этой размерности и размера. Следует отметить, что возвращаемый массив не является копией исходного, а ссылается на те же значения в памяти, что и исходный массив. В этом случае возвращается одномерный массив в первой позиции (0). Использование одиночного индекса в возвращенном массиве приводит к возврату одного элемента. То есть:
>>> x[0][2] 2
Обратите внимание, что x[0,2] = x[0][2] хотя во втором случае это менее эффективно, так как после первого индекса создается новый временный массив, который затем индексируется значением 2.
Примечание для тех, кто знаком с порядком памяти IDL или Fortran, относящимся к индексированию. NumPy использует индексирование по порядку C. Это означает, что последний индекс обычно представляет собой наиболее быстро изменяемое местоположение в памяти, в отличие от Fortran или IDL, где первый индекс представляет собой наиболее быстро изменяемое местоположение в памяти. Эта разница может привести к большой путанице.
Другие варианты индексирования
Можно использовать срезы и шаги для извлечения массивов с тем же количеством размерностей, но с разными размерами по сравнению с исходным. Срезы и шаги работают точно так же, как и для списков и кортежей, за исключением того, что они могут применяться к нескольким размерностям. Лучше всего продемонстрировать это на нескольких примерах:
>>> x = np.arange(10)
>>> x[2:5]
array([2, 3, 4])
>>> x[:-7]
array([0, 1, 2])
>>> x[1:7:2]
array([1, 3, 5])
>>> y = np.arange(35).reshape(5,7)
>>> y[1:5:2,::3]
array([[ 7, 10, 13],
[21, 24, 27]])
Обратите внимание, что срезы массивов не копируют данные внутреннего массива, а только создают новые представления исходных данных. Это отличается от срезов списков или кортежей, и явное copy() рекомендуется, если исходные данные больше не требуются.
Можно индексировать массивы с помощью других массивов для выбора списков значений из массивов в новые массивы. Существует два разных способа достижения этого. Один использует один или несколько массивов значений индексов. Другой предполагает предоставление булевого массива соответствующей формы для указания выбираемых значений. Массивы индексов — очень мощный инструмент, который позволяет избежать циклического перебора отдельных элементов в массивах и, таким образом, значительно повысить производительность.
Можно использовать специальные функции, чтобы эффективно увеличить количество размерностей в массиве при индексировании, чтобы полученный массив приобрел форму, необходимую для использования в выражении или с определенной функцией.
Массивы индексов
Массивы NumPy можно индексировать с помощью других массивов (или любого другого объекта-последовательности, который может быть преобразован в массив, например, списки, за исключением кортежей; см. конец этого документа, чтобы понять почему). Использование массивов индексов варьируется от простых и понятных случаев до сложных и труднопонятных. Во всех случаях с массивами индексов возвращается копия исходных данных, а не представление, как это происходит для срезов.
Массивы индексов должны быть целого типа. Каждое значение в массиве указывает, какое значение в массиве использовать вместо индекса. Для иллюстрации:
>>> x = np.arange(10,1,-1) >>> x array([10, 9, 8, 7, 6, 5, 4, 3, 2]) >>> x[np.array([3, 3, 1, 8])] array([7, 7, 9, 2])
Массив индексов, состоящий из значений 3, 3, 1 и 8, соответственно, создает массив длиной 4 (так же, как и массив индексов), где каждый индекс заменяется значением, которое имеет массив индексов в индексируемом массиве.
Допускаются отрицательные значения, и они работают так же, как и одиночные индексы или срезы:
>>> x[np.array([3,3,-3,8])] array([7, 7, 4, 2])
Ошибка возникает при использовании значений индексов за пределами границ:
>>> x[np.array([3, 3, 20, 8])] <type 'exceptions.IndexError'>: index 20 out of bounds 0<=index<9
В общем случае, при использовании массивов индексов возвращается массив с такой же формой, как и массив индексов, но с типом и значениями индексируемого массива. В качестве примера можно использовать многомерный массив индексов:
>>> x[np.array([[1,1],[2,3]])]
array([[9, 9],
[8, 7]])
Индексирование многомерных массивов
Ситуация усложняется при индексировании многомерных массивов, особенно многомерными массивами индексов. Они часто используются в необычных случаях, но допускаются и полезны для некоторых задач. Начнем с самого простого многомерного случая (используя массив y из предыдущих примеров):
>>> y[np.array([0,2,4]), np.array([0,1,2])] array([ 0, 15, 30])
В этом случае, если массивы индексов имеют совпадающую форму, и для каждой размерности индексируемого массива есть массив индексов, результирующий массив имеет такую же форму, как и массивы индексов, а значения соответствуют набору индексов для каждой позиции в массивах индексов. В данном примере первое значение индекса для обоих массивов индексов равно 0, поэтому первое значение результирующего массива равно y[0,0]. Следующее значение равно y[2,1], а последнее — y[4,2].
Если массивы индексов не имеют одинаковой формы, происходит попытка их преобразования к одинаковой форме. Если они не могут быть преобразованы к одинаковой форме, возникает исключение:
>>> y[np.array([0,2,4]), np.array([0,1])] <type 'exceptions.ValueError'>: shape mismatch: objects cannot be broadcast to a single shape
Механизм векторизации позволяет объединять массивы индексов со скалярами для других индексов. В результате скалярное значение используется для всех соответствующих значений массивов индексов:
>>> y[np.array([0,2,4]), 1] array([ 1, 15, 29])
Переходя к следующему уровню сложности, можно частично индексировать массив с помощью массивов индексов. Нужно немного подумать, чтобы понять, что происходит в таких случаях. Например, если использовать только один массив индексов с y:
>>> y[np.array([0,2,4])]
array([[ 0, 1, 2, 3, 4, 5, 6],
[14, 15, 16, 17, 18, 19, 20],
[28, 29, 30, 31, 32, 33, 34]])
В результате создается новый массив, где каждое значение массива индексов выбирает одну строку из индексируемого массива, и результирующий массив имеет результирующую форму (количество элементов индекса, размер строки).
Пример использования: таблица поиска цветов, где мы хотим отобразить значения изображения в тройки RGB для отображения. Таблица поиска может иметь форму (nlookup, 3). Индексирование такого массива изображением с формой (ny, nx) с dtype=np.uint8 (или любым целочисленным типом, при условии, что значения находятся в пределах таблицы поиска) приведет к массиву формы (ny, nx, 3), где тройка значений RGB связана с каждой точкой пикселя.
В общем случае форма результирующего массива будет представлять собой конкатенацию формы массива индексов (или формы, к которой все массивы индексов были преобразованы) с формой любых неиспользованных размерностей (неиндексированных) в индексируемом массиве.
Булевы или «маскирующие» массивы индексов
Булевы массивы, используемые в качестве индексов, обрабатываются совершенно иначе, чем массивы индексов. Булевы массивы должны иметь такую же форму, как и начальные размерности индексируемого массива. В самом простом случае булев массив имеет такую же форму:
>>> b = y>20 >>> y[b] array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34])
В отличие от случая с целочисленными массивами индексов, в булевом случае результатом является одномерный массив, содержащий все элементы индексируемого массива, соответствующие всем истинным элементам булева массива. Элементы индексируемого массива всегда итерируются и возвращаются в строчно-главном (стиле C) порядке. Результат также идентичен y[np.nonzero(b)]. Как и в случае с массивами индексов, возвращается копия данных, а не представление, как в случае со срезами.
Результат будет многомерным, если y имеет больше размерностей, чем b. Например:
>>> b[:,5] # use a 1-D boolean whose first dim agrees with the first dim of y
array([False, False, False, True, True])
>>> y[b[:,5]]
array([[21, 22, 23, 24, 25, 26, 27],
[28, 29, 30, 31, 32, 33, 34]])
Здесь выбираются 4-я и 5-я строки из индексируемого массива и объединяются в двумерный массив.
В общем случае, когда булев массив имеет меньше размерностей, чем индексируемый массив, это эквивалентно y[b, …], что означает индексирование y с помощью b, за которым следуют столько двоеточий, сколько необходимо для заполнения ранга y. Таким образом, форма результата — одна размерность, содержащая количество истинного значения булева массива, за которой следуют оставшиеся размерности индексируемого массива.
Например, использование двумерного булева массива формы (2,3) с четырьмя истинными элементами для выбора строк из трехмерного массива формы (2,3,5) приводит к двумерному результату формы (4,5):
>>> x = np.arange(30).reshape(2,3,5)
>>> x
array([[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],
[[15, 16, 17, 18, 19],
[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29]]])
>>> b = np.array([[True, True, False], [False, True, True]])
>>> x[b]
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29]])
Дополнительную информацию см. в справочной документации numpy по индексированию массивов.
Объединение массивов индексов со срезами
Массивы индексов могут быть объединены со срезами. Например:
>>> y[np.array([0, 2, 4]), 1:3]
array([[ 1, 2],
[15, 16],
[29, 30]])
По сути, операция со срезом и массивом индексов независимы. Операция со срезом извлекает столбцы с индексом 1 и 2 (т.е. второй и третий столбцы), а затем операция с массивом индексов извлекает строки с индексом 0, 2 и 4 (т.е. первую, третью и пятую строки).
Это эквивалентно:
>>> y[:, 1:3][np.array([0, 2, 4]), :]
array([[ 1, 2],
[15, 16],
[29, 30]])
Аналогично, срезы можно объединять с векторизованными булевыми индексами:
>>> b = y > 20
>>> b
array([[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[ True, True, True, True, True, True, True],
[ True, True, True, True, True, True, True]])
>>> y[b[:,5],1:3]
array([[22, 23],
[29, 30]])
Инструменты структурного индексирования
Для удобного соответствия формы массива выражениям и при присваивании можно использовать объект np.newaxis внутри индексов массива для добавления новых размерностей размером 1. Например:
>>> y.shape (5, 7) >>> y[:,np.newaxis,:].shape (5, 1, 7)
Обратите внимание, что новых элементов в массиве нет, просто увеличивается размерность. Это может быть удобно для объединения двух массивов, что в противном случае потребовало бы явных операций изменения формы. Например:
>>> x = np.arange(5)
>>> x[:,np.newaxis] + x[np.newaxis,:]
array([[0, 1, 2, 3, 4],
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
[4, 5, 6, 7, 8]])
Синтаксис эллипса может использоваться для указания полного выбора любых оставшихся неуказанных размерностей. Например:
>>> z = np.arange(81).reshape(3,3,3,3)
>>> z[1,...,2]
array([[29, 32, 35],
[38, 41, 44],
[47, 50, 53]])
Это эквивалентно:
>>> z[1,:,:,2]
array([[29, 32, 35],
[38, 41, 44],
[47, 50, 53]])
Присваивание значений индексированным массивам
Как уже упоминалось, можно выбрать подмножество массива для присваивания, используя одиночный индекс, срезы и массивы индексов и масок. Присваиваемое индексированному массиву значение должно быть совместимо по форме (иметь ту же форму или быть совместимым с формой, которая получается в результате индексирования). Например, разрешено присвоить константу срезу:
>>> x = np.arange(10) >>> x[2:7] = 1
или массив правильного размера:
>>> x[2:7] = np.arange(5)
Обратите внимание, что присваивания могут привести к изменениям, если присваиваются типы более высокого уровня к типам более низкого уровня (например, float к int) или даже к исключениям (присваивание complex к float или int):
>>> x[1] = 1.2 >>> x[1] 1 >>> x[1] = 1.2j TypeError: can't convert complex to int
В отличие от некоторых ссылок (например, индексов массивов и масок), присваивания всегда производятся к исходным данным в массиве (поскольку иначе не имеет смысла!). Однако следует учитывать, что некоторые действия могут не работать так, как можно ожидать наивно. Этот конкретный пример часто вызывает удивление:
>>> x = np.arange(0, 50, 10) >>> x array([ 0, 10, 20, 30, 40]) >>> x[np.array([1, 1, 3, 1])] += 1 >>> x array([ 0, 11, 20, 31, 40])
Где ожидается, что первая позиция будет увеличена на 3. На самом деле, она будет увеличена только на 1. Причина в том, что из исходного (как временный) извлекается новый массив, содержащий значения 1, 1, 3, 1, затем к временному значению добавляется 1, а затем временный присваивается обратно исходному массиву. Таким образом, значение массива в x[1]+1 присваивается x[1] три раза, а не увеличивается 3 раза.
Обработка переменного числа индексов в программах
Синтаксис индексов очень мощный, но ограничен при работе с переменным числом индексов. Например, если вы хотите написать функцию, которая может обрабатывать аргументы с различным числом измерений, не написав специального кода для каждого возможного числа измерений, как это можно сделать? Если в индекс передается кортеж, кортеж будет интерпретироваться как список индексов. Например (используя предыдущее определение массива z):
>>> indices = (1,1,1,1) >>> z[indices] 40
Таким образом, можно использовать код для построения кортежей с любым количеством индексов и затем использовать их в индексе.
Срезы могут быть заданы в программах с помощью функции slice() в Python. Например:
>>> indices = (1,1,1,slice(0,2)) # same as [1,1,1,0:2] >>> z[indices] array([39, 40])
Аналогично, эллипсис может быть задан кодом с помощью объекта Ellipsis:
>>> indices = (1, Ellipsis, 1) # same as [1,...,1]
>>> z[indices]
array([[28, 31, 34],
[37, 40, 43],
[46, 49, 52]])
По этой причине можно использовать вывод функции np.nonzero() непосредственно в качестве индекса, поскольку она всегда возвращает кортеж массивов индексов.
Из-за специального обращения с кортежами они не преобразуются автоматически в массив, как это было бы со списком. В качестве примера:
>>> z[[1,1,1,1]] # produces a large array
array([[[[27, 28, 29],
[30, 31, 32], ...
>>> z[(1,1,1,1)] # returns a single value
40
© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.19/user/basics.indexing.html