Урок курса
View и copy: предсказание влияния изменений среза на исходный массив
NumPy и pandas: практический тренажёрСрезы в NumPy вы уже использовали — синтаксис start:stop:step знаком с урока по индексации. Но за этим синтаксисом скрывается поведение, которое легко упустить: срез не копирует данные, а возвращает другой «взгляд» на те же байты в памяти. Именно это поведение определяет, что произойдёт с исходным массивом, если вы изменили срез — и этому посвящён весь урок.
View и copy: два режима доступа к памяти ndarray
Когда вы пишете v = a[1:4], NumPy не выделяет новый кусок памяти под три элемента. Вместо этого v — это отдельный объект ndarray, который знает: «мои данные начинаются вот здесь в памяти, шаг такой-то, длина такая-то». Байты, в которых хранятся числа, общие — и у a, и у v. Такой объект называется view (представление).
Последствие прямое: если вы изменяете элемент через v, вы фактически обращаетесь к тем же байтам в памяти, на которые смотрит a. Никакой «защиты» нет — это не баг, а намеренное поведение NumPy ради экономии памяти и скорости.
import numpy as np
a = np.array([10, 20, 30, 40, 50])
v = a[1:4] # view: элементы 20, 30, 40
v[0] = 99 # меняем первый элемент view
print(a) # [10 99 30 40 50] — исходный тоже изменился
Это и есть view в действии: v[0] и a[1] — один и тот же элемент в общей области памяти.
Если вам нужна независимая копия — объект, который хранит те же значения, но в отдельном куске памяти — нужно явно попросить об этом:
a2 = np.array([10, 20, 30, 40, 50])
c = a2[1:4].copy() # независимая копия
c[0] = 99
print(a2) # [10 20 30 40 50] — исходный не тронут
Метод .copy() выделяет новое хранилище и переписывает туда значения. После этого c и a2 ничего не знают друг о друге на уровне памяти.
Разница между view и copy — не в значениях (при создании они одинаковы), а в том, где эти значения живут. View смотрит в чужую память, copy владеет своей. Это разграничение и определяет всё поведение, которое мы разберём дальше.
Проверка общей памяти через np.shares_memory
Понять концептуально, что view разделяет память с оригиналом, — хорошо. Но когда нужно убедиться в этом программно, есть конкретный инструмент: np.shares_memory(a, b).
Функция возвращает True, если массивы a и b делят хотя бы один общий байт памяти. Если их данные полностью независимы — вернёт False. Никакого анализа значений, никаких сравнений элементов — только проверка адресов.
import numpy as np
a = np.array([10, 20, 30, 40, 50])
# Срез — view: данные общие
v = a[1:4]
print(np.shares_memory(a, v)) # True
Теперь создадим копию через .copy() и проверим то же самое:
c = a[1:4].copy()
print(np.shares_memory(a, c)) # False
.copy() запрашивает у NumPy новый кусок памяти и копирует туда значения. После этого c — полностью самостоятельный объект. Связи с a на уровне байтов нет, и np.shares_memory это честно подтверждает.
Важный момент: .copy() можно вызывать как на срезе, так и на целом массиве. Оба варианта дают одинаковый результат — независимое хранилище:
full_copy = a.copy()
print(np.shares_memory(a, full_copy)) # False
np.shares_memory стоит использовать как основной инструмент всякий раз, когда есть сомнение: «это view или копия?». Сравнение значений здесь не поможет — при создании view и copy содержат одинаковые числа, разница только в том, где они хранятся.
Предсказание и наблюдение мутации через view и copy
Алгоритм предсказания сводится к трём шагам:
- Определить, как получен объект. Срез
start:stop:step— view. Вызов.copy()— независимое хранилище. - Если view — любая запись в него затрагивает исходный массив в соответствующих позициях.
- Если копия — исходный массив не изменится ни при каких мутациях копии.
Чтобы алгоритм был реально применим, нужно уметь заранее называть конкретные индексы, которые изменятся. Разберём это на примере с шаговым срезом и двумерным массивом — они чуть менее очевидны, чем простой a[1:4].
Шаговый срез: предсказываем затронутые индексы
import numpy as np
a = np.array([0, 10, 20, 30, 40, 50, 60])
v = a[1:7:2] # элементы с индексами 1, 3, 5 → значения 10, 30, 50
# Предсказание:
# v[0] → a[1], v[1] → a[3], v[2] → a[5]
# Меняем v[1] = 99 → ожидаем a[3] == 99, остальные не трогаем
v[1] = 99
print(a) # [ 0 10 20 99 40 50 60]
Прежде чем запустить код, стоит явно выписать соответствие: v[k] отображается на a[start + k * step]. Здесь start=1, step=2, поэтому v[1] → a[1 + 1*2] = a[3]. Предсказание подтверждается наблюдением.
Теперь мутация в обратную сторону — из исходного массива в view:
a[5] = 777 # a[5] входит в срез v как v[2]
print(v) # [ 10 99 777]
Общая область памяти работает в обе стороны: запись через a видна в v точно так же, как запись через v видна в a.
Двумерный массив: срез строк — тоже view
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
rows = m[0:2] # первые две строки — view
# Предсказание: rows[1, 2] → m[1, 2]
# Меняем rows[1, 2] = 99 → ожидаем m[1, 2] == 99
rows[1, 2] = 99
print(m)
# [[1 2 3]
# [4 5 99]
# [7 8 9]]
Прямоугольный срез по строкам (синтаксис start:stop без fancy indexing) возвращает view — это тот же принцип, что и для одномерного случая.
Когда нужна изоляция: копия блокирует мутацию
m2 = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
rows_copy = m2[0:2].copy()
rows_copy[1, 2] = 99 # меняем копию
print(m2)
# [[1 2 3]
# [4 5 6] ← не изменилось
# [7 8 9]]
Если вы не уверены, является ли объект view или копией — проверьте np.shares_memory до того, как что-то менять. Это особенно важно, когда объект передан в функцию и вы не контролируете, как он был создан.
Атрибут .base: иллюстрация владения памятью и его ограничения
Каждый ndarray хранит атрибут .base. Если массив владеет своими данными сам — .base равен None. Если массив смотрит в чужую память (то есть является view) — .base указывает на объект, которому эта память принадлежит.
В простом случае это выглядит понятно:
import numpy as np
a = np.array([10, 20, 30, 40, 50])
v = a[0:3] # view от a
print(v.base is a) # True — v смотрит в память a
c = a.copy()
print(c.base is None) # True — c владеет своими данными
Для этих двух случаев .base читается легко и интуитивно. На этом его удобство заканчивается.
Проблема возникает в цепочках. Если вы берёте срез от среза, .base у конечного объекта будет указывать не на ближайшего «родителя», а на того, кто реально владеет памятью — то есть на исходный массив или даже на какой-то промежуточный объект, в зависимости от внутреннего устройства NumPy:
a = np.array([10, 20, 30, 40, 50])
v1 = a[1:5] # view от a
v2 = v1[0:2] # view от v1 — но чья память?
print(v2.base is v1) # может быть False
print(v2.base is a) # может быть True
Здесь v2.base is v1 может дать False, потому что NumPy «пробрасывает» .base к реальному владельцу данных, минуя промежуточные объекты. Если вы рассчитываете на v2.base is v1 и получаете False, это не значит, что v2 — копия. Просто .base работает не так, как вы ожидаете.
Ещё одна ловушка: .base отвечает на вопрос «кто владеет памятью», но не на вопрос «разделяют ли вот эти два конкретных объекта память». Если у вас есть x и y, и вы хотите знать, связаны ли они, проверка x.base is y или y.base is x не гарантирует правильного ответа во всех сценариях.
Для этого есть np.shares_memory(x, y) — он отвечает именно на этот вопрос, без допущений о структуре цепочки.
.base полезен как быстрая иллюстрация: увидеть, что view ссылается на чужую память, а копия — нет. Но принимать на его основе решения в коде не стоит. Если нужна надёжная проверка — np.shares_memory.
Задача 1
Дан массив a = np.array([1, 2, 3, 4, 5]). Выполнены команды:
v = a[1:3]
v[0] = 99
Чему равен a[1] после этих операций?
Задача 2
Дан код:
import numpy as np
a = np.array([1, 2, 3, 4, 5])
c = a.copy()
c[0] = 99
Чему равен a[0] после выполнения этих строк?
Задача 3
View vs copy: предсказание и проверка мутации через shares_memory
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая демонстрирует разницу между view и copy на одном массиве:
- Читает из
stdinодну строку с целыми числами через пробел. Создаёт одномерный массивoriginalчерезnp.array(..., dtype=int). - Читает вторую строку с двумя целыми числами
startиstop— границы среза. - Читает третью строку с двумя целыми числами
idxиval— позиция и новое значение для мутации. - Получает view
v = original[start:stop]и копиюc = original[start:stop].copy(). - Выводит строку
shares_view=<bool>— результатnp.shares_memory(original, v). - Выводит строку
shares_copy=<bool>— результатnp.shares_memory(original, c). - Выполняет
v[idx] = val. Выводит строкуoriginal_after_view=<значения через пробел>— текущие значенияoriginal. - Выполняет
c[idx] = val. Выводит строкуoriginal_after_copy=<значения через пробел>— текущие значенияoriginal.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
<n целых чисел через пробел> # элементы original
<start> <stop> # границы среза
<idx> <val> # позиция и значение для мутации
Гарантируется: 0 <= start < stop <= len(original), 0 <= idx < stop - start.
Формат вывода
shares_view=<True|False>
shares_copy=<True|False>
original_after_view=<значения через пробел>
original_after_copy=<значения через пробел>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 4
View и copy на столбцовом срезе двумерного массива: изоляция мутации
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая демонстрирует разницу между view и copy на двумерном массиве:
- Читает из
stdinдва целых числаRиC— число строк и столбцов. - Читает следующие
Rстрок; каждая содержит ровноCцелых чисел через пробел. Создаёт двумерный массивMчерезnp.array(..., dtype=int)формы(R, C). - Читает одну строку с двумя целыми числами
r0иr1— границы среза строк. - Читает одну строку с двумя целыми числами
mrowиmcol— позиция для мутации внутри среза (индексы относительно среза). - Читает одно целое число
val— новое значение. - Получает view
vw = M[r0:r1, :]и копиюcp = M[r0:r1, :].copy(). - Выводит строку
shares_vw=<True|False>— результатnp.shares_memory(M, vw). - Выводит строку
shares_cp=<True|False>— результатnp.shares_memory(M, cp). - Выполняет
vw[mrow, mcol] = val. Выводит строкуM_after_vw=<строки M через ; каждая строка — значения через пробел>. - Выполняет
cp[mrow, mcol] = val. Выводит строкуM_after_cp=<строки M через ; каждая строка — значения через пробел>.
Строки матрицы в выводе разделяются символом ;, значения внутри строки — пробелом.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
R C
<R строк по C целых чисел через пробел>
r0 r1
mrow mcol
val
Гарантируется: 0 <= r0 < r1 <= R, 0 <= mrow < r1 - r0, 0 <= mcol < C.
Формат вывода
shares_vw=<True|False>
shares_cp=<True|False>
M_after_vw=<строки через ;>
M_after_cp=<строки через ;>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Найди и исправь: три ошибки в view, copy и проверке shares_memory
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:
fragmentсоздаётся как срезoriginal[a:b]без.copy(), хотя по условиюfragmentдолжен быть независимой копией, не разделяющей память сoriginal.view_segсоздаётся какoriginal[a:b].copy(), хотя по условиюview_segдолжен быть view — срезом без копирования.- Мутация
view_seg[0] = valпроверяется сравнением сoriginal[0], хотя правильно сравнивать сoriginal[a]— именно этот элемент меняется через view.
Каждая ошибка находится в отдельной строке и не зависит от других.
Что нужно сделать
Найдите и исправьте все три ошибки так, чтобы программа:
- Читала из
stdinодну строку с целыми числами через пробел — элементы массиваoriginal. - Читала вторую строку с тремя целыми числами:
a,b,val— начало среза, конец среза (не включительно) и значение для мутации. - Создавала
fragment = original[a:b].copy()— независимую копию среза. - Создавала
view_seg = original[a:b]— view (срез без копирования). - Выводила
shares_fragment=<True|False>— результатnp.shares_memory(original, fragment)(ожидаетсяFalse). - Выводила
shares_view=<True|False>— результатnp.shares_memory(original, view_seg)(ожидаетсяTrue). - Выполняла
view_seg[0] = val. Выводилаmutated=<True|False>— результат сравненияoriginal[a] == val(ожидаетсяTrue, так как view изменил исходный массив). - Выводила
original=<значения через пробел>— текущие значенияoriginalпосле мутации.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
<целые числа через пробел> # элементы original
<a> <b> <val> # начало среза, конец среза, значение мутации
Гарантируется: 0 <= a < b <= len(original), b - a >= 1.
Формат вывода
shares_fragment=<True|False>
shares_view=<True|False>
mutated=<True|False>
original=<значения через пробел>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
View и copy: умножение фрагмента с контролем мутации исходного массива
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая выполняет два независимых эксперимента с умножением фрагмента массива:
- Читает из
stdinодну строку с целыми числами через пробел — элементы массива. Создаёт одномерный массивsourceчерезnp.array(..., dtype=int). - Читает вторую строку с тремя целыми числами:
a,b,factor— границы среза и множитель. - Эксперимент 1 (через view). Получает
view_seg = source[a:b]. Выполняетview_seg *= factor(умножение на месте). Выводит строкуexp1_source=<значения через пробел>— текущие значенияsource. - Восстанавливает исходные значения: перечитывает или пересоздаёт
sourceиз той же первой строки (сохраните её в переменную заранее). - Эксперимент 2 (через copy). Получает
copy_seg = source[a:b].copy(). Выполняетcopy_seg *= factor. Выводит строкуexp2_source=<значения через пробел>— текущие значенияsource. - Выводит строку
shares_view=<True|False>— результатnp.shares_memory(source, source[a:b])(проверка нового среза от восстановленного массива). - Выводит строку
shares_copy=<True|False>— результатnp.shares_memory(source, source[a:b].copy()).
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
<целые числа через пробел> # элементы source
<a> <b> <factor> # границы среза и множитель
Гарантируется: 0 <= a < b <= len(source), factor >= 2.
Формат вывода
exp1_source=<значения через пробел>
exp2_source=<значения через пробел>
shares_view=<True|False>
shares_copy=<True|False>
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- View и copy: предсказание влияния изменений среза на исходный массив — задача по NumPy и pandas
copy() c[0] = 99 Чему равен a[0] после выполнения этих строк? Проверьте ответ в задаче из бесплатного курса NumPy и pandas.
- NumPy view и copy: почему срез меняет исходный массив
Когда срез NumPy меняет исходный массив и зачем нужен copy()? Пример общей памяти и задача на view, copy и изменение элементов.
- NumPy sharesmemory: view, copy и изменение исходного массива
Исправьте ошибки в работе с представлением и копией ndarray; проверьте общую память через np.sharesmemory.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
