2025, Oct 31 09:16

Почему arr[:][idx] меняет массив, а arr[idx][:] — нет в NumPy

NumPy: базовое и продвинутое индексирование без ловушек. Представление vs копия; почему arr[:][idx] меняет массив а arr[idx][:] — нет, и как писать присваивания

Индексирование в NumPy кажется обманчиво простым — пока цепочки не начинают смешивать базовые срезы с продвинутым индексированием. Достаточно чуть‑чуть поменять порядок операций, и запись «на месте» превратится в тихое изменение временной копии. Ниже — минимальный пример, показывающий эту ловушку и способ её избежать.

Воспроизводимый пример

import numpy as np
idx = np.arange(2, 4)
arr_left = np.arange(5)
arr_left[:][idx] = 0  # изменяет arr_left
print(arr_left)
arr_right = np.arange(5)
arr_right[idx][:] = 0  # НЕ изменяет arr_right
print(arr_right)

В первом присваивании массив действительно меняется. Во втором исходные данные остаются нетронутыми. Дело не в семантике присваивания, а в том, когда вы работаете с представлением, а когда уже перешли к копии.

Что на самом деле происходит

Срез arr_left[:] — это представление той же памяти, что и arr_left. Значит, любые изменения «на месте», сделанные через это представление, всё равно затрагивают исходные данные. Напротив, arr_right[idx] использует продвинутое индексирование и создаёт новый, независимый массив. Как только эта копия появилась, применение [:] действует на копию, а не на arr_right.

Это легко увидеть через атрибут base. Если два массива разделяют одну и ту же область памяти, у представления base указывает на исходный объект; у самостоятельной копии base равен None.

probe_view = arr_left[:]
print(probe_view.base is arr_left)  # True: делят память
probe_copy = arr_right[idx]
print(probe_copy.base)               # None: независимая копия

Хотя arr_left и arr_left[:] — разные объекты Python с разными идентификаторами, у них общий буфер данных NumPy. Поэтому запись через представление всё равно изменяет исходный массив.

Почему порядок важен

Цели присваивания вычисляются слева направо. В arr_left[:][idx] = 0 левая часть arr_left[:] — это представление, затем применяется [idx] к этому представлению, и запись уходит в память arr_left. В arr_right[idx][:] = 0 левая часть arr_right[idx] сразу создаёт копию; потом к ней применяется [:], и присваивание затрагивает только копию.

Короткое решение

Нет причин сцеплять [:][idx] или [idx][:] в такой ситуации. Обращайтесь к целевым элементам напрямую нужными индексами. Если нужно записать в исходный массив, используя продвинутое индексирование, держите продвинутый индекс слева от знака присваивания.

import numpy as np
idx = np.arange(2, 4)
arr = np.arange(5)
arr[idx] = 0  # напрямую изменяет arr
print(arr)

Когда NumPy создаёт представление, а когда копию?

Формулировка NumPy кратко передаёт правило:

Представления создаются, когда элементы можно адресовать с помощью смещений и шагов в исходном массиве. Поэтому базовое индексирование всегда создаёт представления. [...] Продвинутое индексирование, напротив, всегда создаёт копии.

buf = np.arange(5)
buf[:]        # представление (базовое индексирование)
buf[1:3]      # представление (базовое индексирование)
buf[1:3][:]   # представление (базовое индексирование)
buf[[2, 3]]   # копия (продвинутое индексирование)

Цепочки важны, потому что как только вы переключились на продвинутое индексирование и получили копию, дальнейшие срезы применяются уже к копии, а не к оригиналу. Держите продвинутый индекс прямо слева от присваивания, чтобы запись шла в исходный массив.

Замечания по многомерному индексированию

В многомерных массивах предпочитайте a[x, y], когда индексируете разные оси, а не a[x][y]. Во втором случае выполняются две раздельные операции индексирования подряд, из‑за чего легко меняется семантика и появляются лишние копии. Когда x и y — массивы, a[x, y] извлекает попарные элементы [a[xv, yv] for (xv, yv) in zip(x, y)], а не все комбинации x и y. Чтобы получить все комбинации, подход вроде a[x][:, y] меняет характер выбора, но создаёт копию, а не представление.

Почему это важно

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

Выводы

Если нужно менять данные «на месте», ставьте продвинутый индекс прямо слева от присваивания и избегайте цепочек вроде [:][idx] или [idx][:]. Помните: базовые срезы возвращают представления, а продвинутое индексирование — копии. Этой ментальной модели достаточно, чтобы предсказать, попадёт ли запись в исходный массив или уйдёт во временный.

Статья основана на вопросе с StackOverflow от Daniel Reese и ответе пользователя mozway.