10. Копии и представления массивов

Вы, может быть уже заметили, что некоторые функции, что-то делают с массивом, возвращают какой-то результат, но при этом с исходным массивом ничего не происходит. Например:

>>> a = np.arange(18).reshape(3, 6)
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17]])
>>>
>>> a.T
array([[ 0,  6, 12],
       [ 1,  7, 13],
       [ 2,  8, 14],
       [ 3,  9, 15],
       [ 4, 10, 16],
       [ 5, 11, 17]])
>>>
>>> a    #  Исходный массив не изменился
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17]])

Дело в том, что в NumPy существует два понятия: копия массива и представление массива. Попробуем разобраться на примерах.

>>> a = np.arange(18).reshape(3, 6)
>>>
>>> b = a
>>> c = a.T
>>>
>>> #  Можно подумать, что b и c - это 
... #  другие массивы, другие объекты, но
... 
>>> a[0][0] = 777777
>>> a
array([[777777,      1,      2,      3,      4,      5],
       [     6,      7,      8,      9,     10,     11],
       [    12,     13,     14,     15,     16,     17]])
>>> 
>>> b
array([[777777,      1,      2,      3,      4,      5],
       [     6,      7,      8,      9,     10,     11],
       [    12,     13,     14,     15,     16,     17]])
>>>
>>> c
array([[777777,      6,     12],
       [     1,      7,     13],
       [     2,      8,     14],
       [     3,      9,     15],
       [     4,     10,     16],
       [     5,     11,     17]])

Вот здесь у новичков и начинаются: путаница, непонимание и ошибки. Хотя все довольно просто. Итак, когда мы выполнили присваивание b = a, то на самом деле никакого копирования данных не произошло. То есть в памяти компьютера по прежнему находится один массив, а переменные a и b на самом деле даже не переменные, а указатели, которые указывают на одни и те же данные в памяти. Именно поэтому обращаясь по разным указателям к одним и тем же данным мы видим одно и то же.

Хорошо, a и b - это указатели. Как быть с переменной с? По сути это тоже указатель, который ссылается на туже самую область памяти с данными, на которую ссылаются a и b, но представлены эти данные в другой форме. Поэтому в NumPy и существует понятие - представление массива. Действительно, одни и те же данные могут быть представлены в разной форме:

>>> a = np.arange(18).reshape(3, 6)
>>> b = a.reshape(2, 9)
>>> c = a.reshape(2, 3, 3)
>>>
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17]])
>>>
>>> b
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8],
       [ 9, 10, 11, 12, 13, 14, 15, 16, 17]])
>>>
>>> c
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]]])
>>> 
>>> #  Но если изменить один элемент через а
... #  то это отразится во всех представлениях
...
>>> a[0][0] = 7777777
>>>
>>> b
array([[7777777,       1,       2,       3,       4,       5,       6,
              7,       8],
       [      9,      10,      11,      12,      13,      14,      15,
             16,      17]])
>>>
>>> c
array([[[7777777,       1,       2],
        [      3,       4,       5],
        [      6,       7,       8]],

       [[      9,      10,      11],
        [     12,      13,      14],
        [     15,      16,      17]]])
>>> 

Более того, раз все представления ссылаются на одни и те же данные то изменение элементов в одном представлении отразится на всех других представлениях и даже указателях, которые указывают на те же самые данные:

>>> b[0][0] = 1111111
>>> c[0][1][0] = 222222
>>>
>>> b
array([[1111111,       1,       2,  222222,       4,       5,       6,
              7,       8],
       [      9,      10,      11,      12,      13,      14,      15,
             16,      17]])
>>>
>>> c
array([[[1111111,       1,       2],
        [ 222222,       4,       5],
        [      6,       7,       8]],

       [[      9,      10,      11],
        [     12,      13,      14],
        [     15,      16,      17]]])
>>>
>>> a
array([[1111111,       1,       2,  222222,       4,       5],
       [      6,       7,       8,       9,      10,      11],
       [     12,      13,      14,      15,      16,      17]])

Мы можем говорить о копировании, только если данные в памяти физически скопировались в другое место. И как мы убедились, простое присваивание не выполняет такого копирования. Еще мы теперь знаем, что одни и те же данные могут иметь разные представления. Но путаница еще сохраняется, не так ли?

Всё равно непонятно, как теперь правильно выражаться говоря о коде NumPy? И вообще, существует ли в самом NumPy какая-нибудь терминологии на этот случай? Давайте разберемся окончательно и закрепим все это на примерах.


10.1. Присваиванием массивы не копируются

Простое присваивание не делает никаких копий массива - это первое о чем нужно помнить. Да, по началу это кажется странной прихотью создателей NumPy (или самого Python), но если бы это было не так, то нам пришлось бы работать с памятью напрямую, либо самостоятельно беспокоиться об использовании памяти. Поэтому отсутствие автоматического копирования при присваивании - небольшая плата за простоту и легкость Python.

>>> #  Давайте создадим массив с именем "a"
... 
>>> a = np.arange(12)
>>> #  Теперь мы можем сказать: 'У нас есть массив "а"!'
... 
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>>
>>> #  Теперь сделаем копию массива "а"
... 
>>> b = a
>>> #  Казалось бы, что у нас теперь 2 массива
... 
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> b
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> 
>>> b is a    #  Однако массив "b" это массив "a"
True
>>> #  Более того это один и тот же объект:
... 
>>> id(a)
2968935744
>>> id(b)
2968935744
>>>
>>> id(a) == id(b)
True

Самое любопытное, что массивы a и b это действительно один и тот же массив, то есть не просто одни и те же данные, но и тип данных.

>>> a.dtype    #  Тип элементов массива "а"
dtype('int32')
>>> b.dtype    #  Тип элементов массива "b"
dtype('int32')
>>>
>>> a.dtype = np.float64    #  Меняем тип элементов массива "а"
>>> a.dtype
dtype('float64')
>>> b.dtype    #  Тип элементов массива "b" так же меняется
dtype('float64')

В таких ситуациях, даже с математической точки зрения, мы можем говорить, что у нас действительно есть два массива: массив a и массив b, но мы должны обязательно уточнять (и помнить сами), что это в самом прямом смысле один и тот же массив.


10.2. Копирование массивов

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

>>> a = np.arange(12).reshape(2,6)
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])
>>>
>>> b = a.copy()
>>> b
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])
>>>
>>> b == a    #  Все элементы массивов равны
array([[ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True]], dtype=bool)
>>>
>>> b is a     #  Но теперь это уже не один и тот же объект
False
>>>
>>> # Теперь, это разные объекты даже в памяти
...
>>> id(a)
2968935824
>>> id(b)
2968935624
>>>
>>> id(a) == id(b)
False
>>>
>>> a[0][0] = 7777777    #  Изменения в одном не отразятся на другом
>>> a
array([[7777777,       1,       2,       3,       4,       5],
       [      6,       7,       8,       9,      10,      11]])
>>> b
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])
>>>
>>> a.dtype
dtype('int32')
>>> b.dtype    #  Массив "b" скопировал даже тип элементов
dtype('int32')
>>> a.dtype = np.int16    #  Изменим тип элементов массива "а"
>>> a.dtype
dtype('int16')
>>> b.dtype     #  Тип элементов массива "b" не меняется
dtype('int32')

Вот теперь, если мы говорим о массивах a и b, то мы говорим о реальных копиях: у них одинаковые данные, но это не одни и те же данные. Когда мы говорим или слышим, что массив b - это копия массива a, то именно это и подразумевается в терминологии NumPy.


10.3. Представления массивов

Теперь мы знаем, что простое присваивание не выполняет копирования массивов. Если нам нужна копия, то мы можем легко сделать ее с помощью метода copy. Однако, бывают случаи когда копия массива не нужна, а нужен тот же самый массив но с другими размерами - другое представление исходного массива.

Для таких нужд NumPy предоставляет метод ndarray.view(). Этот метод создает новый объект массива, который просматривает данные исходного массива, но изменение размеров одного массива не повлечет изменение размеров другого.

>>> a = np.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>>
>>> b = a
>>> b
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>>
>>> a.shape = 3, 4     #  Меняем размеры массива "а"
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> b    #  Размеры массива "b" так же изменились
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> #  Теперь создадим представление массива "а"
... 
>>> c = a.view()
>>> c
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> a.shape = 2, 6    #  Изменим размеры массива "а"
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])
>>>
>>> c    #  Размеры массива "с" не изменились
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> #  Но массив "с" просматривает те же данные что и "а"
... #  Изменение элементов в "а" отразится на массиве "с"
...
>>> a[0][0] = 11111
>>> c
array([[11111,     1,     2,     3],
       [    4,     5,     6,     7],
       [    8,     9,    10,    11]])
>>> #  А изменение элементов в "с" отразится на массиве "а"
>>> c[0][0] = 77777
>>> a
array([[77777,     1,     2,     3,     4,     5],
       [    6,     7,     8,     9,    10,    11]])

Как правило, функции меняющие форму и порядок элементов в массивах возвращают именно представление, а не копию массива:

>>> a = np.arange(8)
>>> b = a.reshape(2,4)    #  Массив "b" - это представление массива "а"
>>> c = b.T    #  А вот массив "с" - это представление массива "b"
>>>
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7])
>>> b
array([[0, 1, 2, 3],
       [4, 5, 6, 7]])
>>> c
array([[0, 4],
       [1, 5],
       [2, 6],
       [3, 7]])
>>> 
>>> #  Все три массива просматривают одни и те же данные
... 
>>> c[0][0] = 77777
>>>
>>> c
array([[77777,     4],
       [    1,     5],
       [    2,     6],
       [    3,     7]])
>>> b
array([[77777,     1,     2,     3],
       [    4,     5,     6,     7]])
>>> a
array([77777,     1,     2,     3,     4,     5,     6,     7])

Срезы массивов - это тоже представления массивов:

>>> a = np.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>>
>>> b = a[0:12:2]
>>> b
array([ 0,  2,  4,  6,  8, 10])
>>>
>>> c = a[1:12:2]
>>> c
array([ 1,  3,  5,  7,  9, 11])
>>> 
>>> #  Все три массива просматривают одни и те же данные
... 
>>> b[:] = 0
>>>
>>> b
array([0, 0, 0, 0, 0, 0])
>>> a
array([ 0,  1,  0,  3,  0,  5,  0,  7,  0,  9,  0, 11])
>>>
>>> c[:] = -1
>>> c
array([-1, -1, -1, -1, -1, -1])
>>>
>>> a
array([ 0, -1,  0, -1,  0, -1,  0, -1,  0, -1,  0, -1])

Если мы говорим, что массив b - это представление массива a, то подразумевается, что независимо от формы и вида массива b он состоит из тех же данных в памяти, что и массив a. Поэтому изменение элементов в одном из них повлечет изменение соответствующих элементов в другом.