Базовая индексация: элементы, строки, столбцы и срезы
Содержание курса
Предсказание 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:1 → ceil(3/1) = 3, 0:4:2 → ceil(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), а не вручную.
