NumPy: массивы, форма и индексация

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

Содержание курса

Предсказание shape результата и сводный пример

Теперь можно сформулировать единственное правило, которое объясняет shape любого результата базовой индексации:

  • Целый индекс по оси убирает эту ось из результата.
  • Срез по оси сохраняет ось, длина которой определяется диапазоном среза.

Из этого немедленно следует: если вы задали целый индекс по всем осям — получаете скаляр shape (). Если по одной оси срез, по другой целый индекс — одномерный массив. Если по обеим осям срезы — двумерный массив.

Как вычислить длину среза с шагом. Самый надёжный способ — использовать встроенный метод Python slice(...).indices(n), который применяет те же правила нормализации границ, что и срез NumPy. Метод возвращает нормализованную тройку (start, stop, step) с учётом знака шага, отрицательных границ и выхода за пределы оси длиной n; длина среза — это len(range(*slice(s, e, k).indices(n))).

import numpy as np

n = 5
# Положительный шаг, обычный случай
print(len(range(*slice(1, 4, 1).indices(n))))   # 3  (позиции 1, 2, 3)
print(len(range(*slice(0, 6, 2).indices(n))))   # 3  (позиции 0, 2, 4 — stop обрезан до 5)

# Отрицательный шаг
print(len(range(*slice(None, None, -1).indices(n))))  # 5  ([::-1] по оси длиной 5)
print(len(range(*slice(3, 0, -1).indices(n))))        # 3  (позиции 3, 2, 1)

Для частного случая положительного шага и границ 0 <= start < stop <= n длину можно посчитать вручную по формуле ceil((stop - start) / step). Например, 1:4:1ceil(3/1) = 3, 0:4:2ceil(4/2) = 2. Шаблон ::k по оси длиной n при положительном k даёт ceil(n / k). Но как только шаг становится отрицательным или границы выходят за пределы оси — пользуйтесь slice(...).indices(n), а не формулой.

Проверим правило на одном массиве:

arr = np.arange(12).reshape(3, 4)
# [[ 0,  1,  2,  3],
#  [ 4,  5,  6,  7],
#  [ 8,  9, 10, 11]]

print(arr[1, 2].shape)      # () — два целых индекса, обе оси убраны
print(arr[0].shape)         # (4,) — целый индекс по строкам, срез : по столбцам
print(arr[:, 2].shape)      # (3,) — срез : по строкам, целый индекс по столбцам
print(arr[0:2, 1:3].shape)  # (2, 2) — срезы по обеим осям: 2 строки, 2 столбца
print(arr[::2, ::2].shape)  # (2, 2) — ceil(3/2)=2 строки, ceil(4/2)=2 столбца
print(arr[-1].shape)        # (4,) — отрицательный индекс, та же логика

Для arr[::2, ::2] считаем вручную (шаг положительный, границы в пределах оси): ось строк длиной 3, шаг 2 → ceil(3/2) = 2 строки (индексы 0 и 2); ось столбцов длиной 4, шаг 2 → ceil(4/2) = 2 столбца (индексы 0 и 2). Итого shape (2, 2).

Один нюанс, который легко пропустить: срез 0:1 и целый индекс 0 — разные вещи.

print(arr[0, :].shape)    # (4,)  — ось строк убрана
print(arr[0:1, :].shape)  # (1, 4) — ось строк сохранена, но длина 1

Оба варианта дают «первую строку», но второй сохраняет двумерную структуру. Это важно, когда результат передаётся в функцию или участвует в операции, чувствительной к числу осей.

Кратко: shape результата определяется по каждой оси независимо. Идёте слева направо по индексному выражению — если видите целое число, эту ось вычёркиваете; если видите срез, записываете его длину. Для нетривиальных случаев длину срезов надёжнее считать через slice(...).indices(n), а не вручную.