13. Структурированные массивы

13.1. Введение

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

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

Рассмотрим простой пример:

>>> cars = np.array([('mazda', 4, 3573.5),
                     ('tesla', 2, 4226.81),
                     ('bmv', 8, 3489.21)],
                     dtype = [('model', 'U10'), ('quantity', 'i4'), ('value', 'f4')])
>>> cars
array([('mazda', 4, 3573.5), ('tesla', 2, 4226.81005859375),
       ('bmv', 8, 3489.2099609375)], 
      dtype=[('model', '<U10'), ('quantity', '<i4'), ('value', '<f4')])

В данном примере, cars - это одномерный массив, который состоит из трех элементов: ('mazda', 4, 3573.5), ('tesla', 2, 4226.81) и ('bmv', 8, 3489.21). С первого взгляда это не совсем очевидно, но в этом легко убедиться:

>>> cars.shape
(3,)

Такой массив проще всего представить в виде таблицы, где каждая строка является элементом массива cars. Каждый элемент представляет собой структуру, которая состоит из трех полей (столбцов). В каждом поле хранятся данные определенного типа, которые перечислены в dtype: первое поле, 'model' - это модель автомобиля, представленная в виде строки из символов юникода длинной не более 10 символов; второе поле, 'quantity' - это количество имеющихся автомобилей, представленное в виде 32-х битного целого числа; третье поле, 'value' - это стоимость автомобиля, представленная в виде 32-х битного числа с плавающей точкой.

Первое, что настораживает, так это использование символьных кодов для определения типа данных каждого поля. Конечно, мы можем воспользоваться объектами dtype вместо символьных кодов:

>>> cars = np.array([('mazda', 4, 3573.5),
                     ('tesla', 2, 4226.81),
                     ('bmv', 8, 3489.21)],
             dtype = [('model', np.dtype('unicode')),
                      ('quantity', np.dtype('int32')),
                      ('value', np.dtype('float32'))])

Однако, в выводе массива мы все равно увидим символьные коды типов данных (!) и не увидим марок автомобилей:


>>> cars
array([('', 4, 3573.5), ('', 2, 4226.81005859375), ('', 8, 3489.2099609375)], 
      dtype=[('model', '<U'), ('quantity', '<i4'), ('value', '<f4')])
>>> 

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

Второе, что кажется немного странным - это отсутствие очевидных способов определять длину строк с помощью np.dtype('unicode'), которая по умолчанию равна 0. Ну и третья странность которая бросается в глаза это наличие символов '<' и '> в выводе: dtype=[('model', '<U'), ('quantity', '<i4'), ('value', '<f4')]. Данные символы используются для указания порядка байтов, т.е. способу представления данных в памяти машины. Если число или данные не могут быть представлены одним байтом, то всегда необходимо указывать в каком порядке байты записываются в памяти компьютера. Если самый старший байт сохраняется первым, то это обозначается символом '>', если первым сохраняется самый младший байт, то символом '<'. Чаще всего выбор порядка байтов произволен и определяется простыми соглашениями.

В общем без символьных кодов при создании структурированных массивов обойтись невозможно, а если и возможно, то сделать это очевидными методами вряд ли получится. Можно подумать, что это серьезный недостаток NumPy, но скорее всего это связано с пережитками прошлого предшественника Numeric. В самых разных источниках этот момент вообще никак не освещается. Тем не менее, структурированные массивы NumPy прекрасно работают, очень гибкие и активно используются в самых разных направлениях.

И так, без символьных кодов типов данных при работе с структурированными массивами не обойтись. Чтож, ничего сверхсложного в этом нет:

>>> cars = np.array([('mazda', 4, 3573.5),
                     ('tesla', 2, 4226.81),
                     ('bmv', 8, 3489.21)],
                     dtype = [('model', 'U10'), ('quantity', 'i4'), ('value', 'f4')])
>>> cars
array([('mazda', 4, 3573.5), ('tesla', 2, 4226.81005859375),
       ('bmv', 8, 3489.2099609375)], 
      dtype=[('model', '<U10'), ('quantity', '<i4'), ('value', '<f4')])

Обращаться к элементам структурированного массива можно по индексу:

>>> cars[1]
('tesla', 2, 4226.81005859375)
>>>
>>> cars[1][0]
'tesla'

Для получения доступа к отдельному полю достаточно указать его в индексе:

>>> cars['model']
array(['mazda', 'tesla', 'bmv'], 
      dtype='<U10')
>>>
>>> cars['quantity']
array([4, 2, 8])
>>> 
>>> cars['quantity'][1] = 5    #  Можем изменить значение поля.
>>>
>>> cars
array([('mazda', 4, 3573.5), ('tesla', 5, 4226.81005859375),
       ('bmv', 8, 3489.2099609375)], 
      dtype=[('model', '<U10'), ('quantity', '<i4'), ('value', '<f4')])

Структурированные массивы NumPy работают гораздо быстрее чем аналогичные структуры на языке Python. Тем не менее, для манипулирования такими простыми структурами данных, как таблицы гораздо лучше подойдет пакет pandas. Причина в том, что данный пакет предоставляет уже готовые высокоуровневые интерфейсы для таких манипуляций и к тому же обеспечивает лучшую производительность.

Давайте рассмотрим пример посложнее, когда полем структурированного массива является другой структурированный массив:

>>> cars = np.array([('mazda', 4, 3573.5, ('Japan', 2016)),
                     ('tesla', 2, 4226.81, ('USA', 2017)),
                     ('bmv', 8, 3489.21, ('Germany', 2015))],
            dtype = [('model', 'U10'),
                     ('quantity', 'i4'),
                     ('value', 'f4'),
                     ('production',[('country','U20'),
                                    ('year','i4')])])
>>> 
>>> cars
array([('mazda', 4, 3573.5, ('Japan', 2016)),
       ('tesla', 2, 4226.81005859375, ('USA', 2017)),
       ('bmv', 8, 3489.2099609375, ('Germany', 2015))], 
      dtype=[('model', '<U10'), ('quantity', '<i4'), ('value', '<f4'),
             ('production', [('country', '<U20'), ('year', '<i4')])])

В данном примере мы добавили четвертое поле - 'production' (производство), которое в свою очередь так же является структурированным массивом из двух полей: 'country' (страна производства) и 'year' (год производства). Получить доступ к новому полю можно так же по имени в индексе:

>>> cars['production']
array([('Japan', 2016), ('USA', 2017), ('Germany', 2015)], 
      dtype=[('country', '<U20'), ('year', '<i4')])
>>>
>>> cars['production']['country']
array(['Japan', 'USA', 'Germany'], 
      dtype='<U20')
>>>
>>> cars['production']['year']
array([2016, 2017, 2015])

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

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

>>> #  Структурированный массив заполненный нулями:
... a = np.zeros(4, dtype=[('a', 'i4'), ('b', 'i4')])
>>> a
array([(0, 0), (0, 0), (0, 0), (0, 0)], 
      dtype=[('a', '<i4'), ('b', '<i4')])
>>> 
>>> #  Структурированный массив заполненный единицами:
... b = np.ones(4, dtype=[('a', 'i4'), ('b', 'i4')])
>>> b
array([(1, 1), (1, 1), (1, 1), (1, 1)], 
      dtype=[('a', '<i4'), ('b', '<i4')])
>>> 
>>> #  В данном случае функция empty заполнит массив нулями:
... c = np.empty(4, dtype=[('a', 'i4'), ('b', 'i4')])
>>> c
array([(0, 0), (0, 0), (0, 0), (0, 0)], 
      dtype=[('a', '<i4'), ('b', '<i4')])

13.2. Структурированный тип данных

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

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


13.2.1. Способы создания структурированного типа данных

Создать структурированный тип данных можно с помощью функции numpy.dtype, при этом, есть четыре разных способа сделать это. В зависимости от данных, каждый из них может оказаться более удобным чем остальные, поэтому имеет смысл подробно рассмотреть каждый из них.

13.2.1.1. Список кортежей: один кортеж - одно поле

В данном способе структурированный тип данных определяется как список кортежей. Один кортеж соответствует только одному полю (столбцу) данных. Кортежи имеют следующую форму (имя поля, тип данных, форма подмассива), где параметр форма подмассива является необязательным. Параметр имя поля может быть или строкой или, в случае использования заголовков полей - кортежем. В качестве параметра тип данных может выступать любой объект, который может быть конвертирован в тип данных. Необязательный параметр форма подмассива представляет собой кортеж из целых чисел, определяющих форму массивов, которые будут храниться в данном поле.

>>> dots = np.array([('A', 0.5, [-1,12]),
                     ('B', 0.2, [0, 17]),
                     ('C', 0.19, [1, 21]),
                     ('D', 0.11, [2, 27])],
            dtype = [('char', 'U1'), ('probability', 'f2'), ('coordinate', 'i4', (2))])
>>> 
>>> dots
array([('A', 0.5, [-1, 12]), ('B', 0.199951171875, [0, 17]),
       ('C', 0.18994140625, [1, 21]), ('D', 0.1099853515625, [2, 27])], 
      dtype=[('char', '<U1'), ('probability', '<f2'), ('coordinate', '<i4', (2,))])

В этом примере мы определили список из 4-х элементов, где каждый элемент соответствует одной точке и содержит имя точки, вероятность ее появления и координату на плоскости. Естественно, определяя структурированный тип данного массива мы должны указать три кортежа, по одному на каждое поле. Первое поле - это 'буква' ('char'), которая соответствует определенной точке и мы указали тип этого поля как 'U1' - строка символов юникода длинной один символ. Далее, идет поле - 'вероятность' ('probability') и указали тип 'f2' - вещественное число половинной точности. И наконец, поле - 'координаты точки' ('coordinate'), координатами является массив из двух целых чисел, поэтому мы определили тип данного поля, как 'i4', ну и конечно же если мы упаковываем координаты в массив, то мы обязаны указать его форму (2) - одна ось из двух элементов.

Кстати, мы можем определить структурированный тип заранее, сделав на него ссылку в виде переменной, а потом использовать ее при создании массива:

>>> dots_type = np.dtype([('char', 'U1'), ('probability', 'f2'), ('coordinate', 'i4', (2))])
>>> dots = np.array([('A', 0.5, [-1,12]),
                     ('B', 0.2, [0, 17]),
                     ('C', 0.19, [1, 21]),
                     ('D', 0.11, [2, 27])], dots_type)
>>> dots
array([('A', 0.5, [-1, 12]), ('B', 0.199951171875, [0, 17]),
       ('C', 0.18994140625, [1, 21]), ('D', 0.1099853515625, [2, 27])], 
      dtype=[('char', '<U1'), ('probability', '<f2'), ('coordinate', '<i4', (2,))])
>>>
>>> #  Если подмассив состоит всего из одной оси
... #  то вместо кортежа можно просто указать число
... #  элементов по данной оси:
... dots_type = np.dtype([('', 'U1'), ('', 'f2'), ('', 'i4', 2)])
>>> #                                                     ---^---
...
>>> dots_type   
dtype([('f0', '<U1'), ('f1', '<f2'), ('f2', '<i4', (2,))])

Если параметр имя поля представляет собой пустую строку'', то автоматически полю будет присвоено имя fN, где N - это целочисленный индекс поля, который отсчитывается слева, начиная с нуля.

>>> dots = np.array([('A', 0.5, [-1,12]),
                     ('B', 0.2, [0, 17]),
                     ('C', 0.19, [1, 21]),
                     ('D', 0.11, [2, 27])],
    dots_type = np.dtype([('', 'U1'), ('', 'f2'), ('', 'i4', 2)])
>>>
>>> dots
array([('A', 0.5, [-1, 12]), ('B', 0.199951171875, [0, 17]),
       ('C', 0.18994140625, [1, 21]), ('D', 0.1099853515625, [2, 27])], 
      dtype=[('f0', '<U1'), ('f1', '<f2'), ('f2', '<i4', (2,))])
>>>
>>> #  Обратиться к содержимому полей можно,
... #  указав в индексе имена полей, появившиеся автоматически:
... dots['f0']
array(['A', 'B', 'C', 'D'], 
      dtype='<U1')
>>>
>>> dots['f1']
array([ 0.5       ,  0.19995117,  0.18994141,  0.10998535], dtype=float16)
>>>
>>> dots['f2']
array([[-1, 12],
       [ 0, 17],
       [ 1, 21],
       [ 2, 27]])

13.2.1.2. Указание параметров полей в строке через запятую

Это самый краткий способ создания структурированного типа данных: параметры (за исключением имени поля) перечисляются в одной строке и разделяются запятыми. Именам полей по умолчанию присваиваются имена f0, f1, f2 и т.д.:

>>> probabilities = np.array([('A', 0.5, 8),
...                           ('B', 0.3, 14),
...                           ('C', 0.2, 21)],
...                    dtype = 'U1, f4, i2')
>>> 
>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('f0', '<U1'), ('f1', '<f4'), ('f2', '<i2')])

В этом примере мы создали простой структурированный массив из трех элементов для хранения вероятностей наступления событий - появления определенных чисел. Каждый элемент состоит из трех полей: имени события, вероятности его наступления и самого числа. То есть событие A (тип 'U1') состоит в том что появится число 8 (тип 'i2') и вероятность наступления этого события равна 0.5 (тип 'i2'). Как видно, все три типа мы просто указали в одной строке, при этом имена полей появились автоматически.

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

>>> matrices = np.array([('A', [[1, 2, 3], [3, 1, 2]], [4,3,5]),
                         ('B', [[5, 3, 9], [0, 4, 3]], [5, 7, 11]),
                         ('C', [[2, 2, 5], [1, 0, 1]], [3, 2, 6])],
                  dtype = 'U1, (2, 3)i4, 3u2')
>>> matrices
array([('A', [[1, 2, 3], [3, 1, 2]], [4, 3, 5]),
       ('B', [[5, 3, 9], [0, 4, 3]], [5, 7, 11]),
       ('C', [[2, 2, 5], [1, 0, 1]], [3, 2, 6])], 
      dtype=[('f0', '<U1'), ('f1', '<i4', (2, 3)), ('f2', '<U2', (3,))])

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


13.2.1.3. Словарь массивов с параметрами полей

Это самый гибкий способ определения структурированного типа, так как помимо имени и типа поля он позволяет задавать еще целый ряд параметров.

Словарь имеет два обязательных ключа, 'names' для списка имен полей и 'formats' для списка типов данных каждого поля, причем длина этих двух списков должна быть одинакова. Так же в словаре могут быть указаны еще четыре необязательных опциональных ключа: 'offsets' (байты-смещения), 'itemsize' (размеры элементов), 'aligned' (выравнивание) и 'titles' (заголовки полей). Необязательному ключу 'offsets' должен соответствовать список из целых чисел байтов смещений для каждого поля. Чаще всего байты-смещения не заданы и вычисляются автоматически. Ключу 'itemsize' соответствует одно целое число, которое задает общий размер в байтах всего структурированного типа данных и которое должно быть достаточно большим, так как включает в себя размеры типов данных всех полей.

Необязательный ключ 'aligned' может быть установлен на True, что заставляет вычислять смещения по выровненным смещениям. Ключу 'titles' соответствует список заголовков полей, той же длинны что и список по ключу 'names'.

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'names':['char', 'probability', 'value'],
                               'formats':['U1','f4','i2']})
>>>
>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])
>>>
>>>  #  Можно указать 'offsets' и 'itemsize'
...
>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'names':['char', 'probability', 'value'],
                               'formats':['U1', 'f4', 'i2'],
                               'offsets': [0, 4, 8],
                               'itemsize': 10})
>>> 
>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

Смещения можно указать так, что поля будут перекрываться, однако, это может привести как к потере данных так и к возникновению ошибок.


13.2.1.4. Словарь имен полей и их свойств

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

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

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'char':('U1', 0),
                               'probability':('f4', 4),
                               'value':('i2', 8)})
>>>
>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

Эта форма не рекомендуется еще и потому, что словари в версиях Python до 3.6 не сохраняют порядок элементов, а порядок элементов в словаре должен соответствовать порядку полей в структурированном массиве.


13.2.2. Манипулирование структурированными типами данных и их отображение

Что бы отобразить список имен полей структурированного типа данных, можно воспользоваться атрибутом names объекта dtype:

>>> dots_type = np.dtype([('char', 'U1'), ('probability', 'f2'), ('coordinate', 'i4', (2))])
>>>
>>> dots_type.names
('char', 'probability', 'coordinate')
>>>
#  К именам отдельных полей мы можем обратиться по индексу:
... dots_type.names[0]
'char'
>>> dots_type.names[1]
'probability'

Чтобы изменить имена всех полей достаточно обратиться к атрибуту names и присвоить ему любую последовательность аналогичной длинны, состояющую из строк новых имен:

>>> dots_type.names 
('char', 'probability', 'coordinate')
>>>
>>> dots_type.names = ('letter', 'chance', 'number')
>>>
>>> dots_type.names 
('letter', 'chance', 'number')

Еще объект dtype имеет похожий на словарь атрибут fields, в котором ключами являются имена полей, а значениями - кортежи с типом данных и байтами смещений:

>>> dots_type = np.dtype([('char', 'U1'), ('probability', 'f2'), ('coordinate', 'i4', (2))])
>>> dots_type.fields
mappingproxy({'coordinate': (dtype(('<i4', (2,))), 6), 'char': (dtype('<U1'), 0), 'probability': (dtype('float16'), 4)})

У обычных массивов атрибуты fields и names будут равны None.


13.2.3. Автоматическое вычисление байтов смещения и вырвнивания полей

В NumPy есть два метода автоматического определения байтов смещения поля и общей размерности структурированного типа данных в зависимости от того в какое значение установлен флаг align.

По умолчанию align = False и в таком режиме NumPy собирает все поля вместе, так что все они смежны в памяти и каждое последующее поле начинается с смещения байтов которое закончило предыдущее

>>> x = np.dtype('f2, f4, u1, u1, U1, U2, i4, i1, u1, i8, u2')
>>>
>>> [x.fields[name][1] for name in x.names]
[0, 2, 6, 7, 8, 12, 20, 24, 25, 26, 34]

Мы создали структурированный тип данных из 11 полей и сгенерировали список из байтов смещения каждого поля. Теперь становится видно, что первое поле начинается с 0 и заканчивается 1, т.е ровно два байта - как раз ровно столько, сколько нужно для хранения чисел типа f2. Для хранения чисел типа f4 нужно 4 байта, поэтому второе поле начинается с 2 и заканчивается 5. Примечательно, что для хранения одного символа юникода нужно 4 байта поэтому пятое поле в котором хранится всего один сивол (U1) начинается с 8 и заканчивается 11, а шестое поле в котором два символа (U2) начинается с 12 и заканчивается 19.

Если align = True, то NumPy будет выравнивать поля так же, как и многие компиляторы С, используя C-struct. В некоторых случаях это может привести к увеличению производительности, но при этом байты смещения полей будут расположены по правилам C-struct:

>>> x = np.dtype('f2, f4, u1, u1, U1, U2, i4, i1, u1, i8, u2', align = True)
>>>
>>> [x.fields[name][1] for name in x.names]
[0, 4, 8, 9, 12, 16, 24, 28, 29, 32, 40]

13.2.4. Заголовки полей

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

Что бы добавить заголовки можно воспользоваться двумя способами создания структурированного типа данных. Первый, это словарь массивов с параметрами полей:

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'names':['char', 'probability', 'value'],
                               'formats':['U1', 'f4', 'i2'],
                               'titles':['event name', 'probability of an event', 'event value']})
>>> 
>>> probabilities['event value']
array([ 8, 14, 21], dtype=int16)

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

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = [(('char', 'event name'), 'U1'),
                               (('probability', 'probability of an event'), 'f2'),
                               (('coordinate', 'event value'), 'i4', (2))])
>>>
>>> probabilities['probability of an event']
array([ 0.5       ,  0.30004883,  0.19995117], dtype=float16)

Если при создании структурированного типа данных мы все таки используем словари кортежей, то вместо обычного двухэлементного кортежа используется кортеж из трех элементов: (datatype, offset, title):

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'char':('U1', 0, 'event name'),
                               'probability':('f4', 4, 'probability of an event'),
                               'value':('i2', 8, 'event value')})
>>> 
>>> probabilities['event name']
array(['A', 'B', 'C'], 
      dtype='<U1')

13.3. Присвоение значений структурированному массиву

Существует три способа присвоения значений структурированному массиву: использование кортежей Python, использование скалярных значений или использование других структурированных массивов.


13.3.1. Использование кортежей Python

Это самый простой способ присвоить значения структурированному массиву. Присваиваемое значение должно быть кортежем, длинна которого равна количеству полей структурированного массива. Если использовать списки или массивы NumPy то они будут инициировать правила транслирования массивов и присвоение не выполнится. Элементы кортежа присваиваются последовательно всем полям элемента массива слева направо:

>>> nor = np.array([(1, 2, 3, 4), (5, 6, 7, 8)], dtype='i1, i2, i4, i8')
>>> 
>>> nor
array([(1, 2, 3, 4), (5, 6, 7, 8)], 
      dtype=[('f0', 'i1'), ('f1', '<i2'), ('f2', '<i4'), ('f3', '<i8')])
>>> 
>>> nor[1] = (4, 3, 2, 1)
>>> 
>>> nor
array([(1, 2, 3, 4), (4, 3, 2, 1)], 
      dtype=[('f0', 'i1'), ('f1', '<i2'), ('f2', '<i4'), ('f3', '<i8')])

13.3.2. Использование скалярных значений

Присваивание одного элемента - скаляра, приводит к тому что он будет присвоен всем полям всех элементов массива:

>>> nor = np.array([(1, 2, 3, 4), (5, 6, 7, 8)], dtype='U1, i2, i4, i8')
>>> 
>>> nor[:] = 1
>>> 
>>> nor
array([('1', 1, 1, 1), ('1', 1, 1, 1)], 
      dtype=[('f0', '<U1'), ('f1', '<i2'), ('f2', '<i4'), ('f3', '<i8')])

Если выполнить присваивание массива скаляров длинна которого равна количеству элементов структурированного массива, то каждому элементу структурированного массива будет присвоен соответствующий элемент массива скаляров:

>>> nor = np.array([(1, 2, 3, 4), (5, 6, 7, 8)], dtype='U1, i2, i4, i8')
>>>
>>> nor[:] = np.array([0, 9])
>>> 
>>> nor
array([('0', 0, 0, 0), ('9', 9, 9, 9)], 
      dtype=[('f0', '<U1'), ('f1', '<i2'), ('f2', '<i4'), ('f3', '<i8')])

Обычным массивам можно присвоить структурированный массив, но только если структурированный тип данных имеет только одно поле, иначе, каждому элементу обычного массива будет присвоено значение только первого поля структурированного массива:

>>> #  Неструктурированный массив:
... a = np.array([0, 9])
>>> 
>>> #  Структурированный массив из двух полей:
... b = np.array([(1, 2), (3, 4)], dtype = [('X', 'i4'), ('Y', 'i4')])
>>> b
array([(1, 2), (3, 4)], 
      dtype=[('X', '<i4'), ('Y', '<i4')])
>>> 
>>> #  Структурированный массив с одним полем:
... c = np.array([(1,), (2,)], dtype = [('X', 'i4')])
>>> c
array([(1,), (2,)], 
      dtype=[('X', '<i4')])
>>> 
>>> a[:] = b    #  Приведет к ошибке
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Can't cast from structure to non-structure,
except if the structure only has a single field.
>>>
>>> a[:] = c    #  Не приведет к ошибке
>>> a
array([1, 2])

13.3.3. Использование других структурированных массивов

Присвоение одного структурированного массива другому возможно только если они имеют одинаковое количество полей. Выполняется присвоение по полям:

>>> x = np.array([(1, 2, 3), (4, 5, 6)], dtype=[('a1', 'i2'), ('b1', 'f4'), ('c1', 'i2')])
>>> y = np.array([(6, 5, 4), (3, 2, 1)], dtype=[('a2', 'i2'), ('b2', 'f4'), ('c2', 'i2')])
>>>
>>> x = y
>>> x
array([(6, 5.0, 4), (3, 2.0, 1)], 
      dtype=[('a2', '<i2'), ('b2', '<f4'), ('c2', '<i2')])

Если типы данных различны, то присвоение все равно выполнится, но при этом структурированный тип данных первого массива будет преобразован к типу второго массива:

>>> x = np.array([(1, 2, 3), (4, 5, 6)], dtype=[('a1', 'i2'), ('b1', 'f4'), ('c1', 'i2')])
>>> y = np.array([(6, 5, 4), (3, 2, 1)], dtype=[('a2', 'U10'), ('b2', 'i4'), ('c2', 'f2')])
>>>
>>> x = y
>>> x
array([('6', 5, 4.0), ('3', 2, 1.0)], 
      dtype=[('a2', '<U10'), ('b2', '<i4'), ('c2', '<f2')])

Если структурированные массивы содержат подмассивы, то размеры подмассивов так же должны совпадать, а вот тип данных может и не совпадать:

>>> x = np.array([([[1, 1], [2, 2]], 2, 3),
                  ([[3, 3], [4, 4]], 5, 6)],
           dtype=[('a1', 'i2', (2, 2)), ('b1', 'f4'), ('c1', 'i2')])
>>> x
array([([[1, 1], [2, 2]], 2.0, 3), ([[3, 3], [4, 4]], 5.0, 6)], 
      dtype=[('a1', '<i2', (2, 2)), ('b1', '<f4'), ('c1', '<i2')])
>>>
>>> y = np.array([([[9, 9], [8, 8]], 5, 4),
                  ([[7, 7], [6, 6]], 2, 1)],
           dtype=[('a2', 'U10', (2, 2)), ('b2', 'i4'), ('c2', 'f2')])
>>> y
array([([['9', '9'], ['8', '8']], 5, 4.0),
       ([['7', '7'], ['6', '6']], 2, 1.0)], 
      dtype=[('a2', '<U10', (2, 2)), ('b2', '<i4'), ('c2', '<f2')])
>>>
>>> x = y
>>> x
array([([['9', '9'], ['8', '8']], 5, 4.0),
       ([['7', '7'], ['6', '6']], 2, 1.0)], 
      dtype=[('a2', '<U10', (2, 2)), ('b2', '<i4'), ('c2', '<f2')])

13.4. Индексация структурированных массивов

13.4.1. Доступ к отдельным полям

Получение доступа к отдельным полям структурированного массива, а так же их изменение происходит путем индексации массива с именем необходимого поля:

>>> probabilities = np.array([('A', 0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'char':('U1', 0),
                               'probability':('f4', 4),
                               'value':('i2', 8)})
>>> probabilities['char']
array(['A', 'B', 'C'], 
      dtype='<U1')
>>> 
>>> probabilities['probability']
array([ 0.5       ,  0.30000001,  0.2       ], dtype=float32)
>>> 
>>> probabilities['value']
array([ 8, 14, 21], dtype=int16)
>>> 
>>> probabilities['char'] = 'N'    #  Всем элементам поля присваиваем 'N'
>>> probabilities['char']
array(['N', 'N', 'N'], 
      dtype='<U1')
>>>
>>> #  Присваиваем каждому элементу поля уникальное имя:
... probabilities['char'] = ('X', 'Y', 'Z')
>>> probabilities['char']
array(['X', 'Y', 'Z'], 
      dtype='<U1')
>>>
>>> #  Исходный массив действительно изменен:
... probabilities
array([('X', 0.5, 8), ('Y', 0.30000001192092896, 14),
       ('Z', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

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

>>> probabilities = np.array([('A', 0.5,  8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                      dtype = {'char':('U1', 0),
                               'probability':('f4', 4),
                               'value':('i2', 8)})
>>>
>>> probabilities['char']    #  Это представление массива probabilities
array(['A', 'B', 'C'], 
      dtype='<U1')
>>> 
>>> probabilities['char'] = 'N'    #  Меняем представление...
>>> probabilities['char']
array(['N', 'N', 'N'], 
      dtype='<U1')
>>> 
>>> probabilities     #  ... автоматически меняем исходный массив
array([('N', 0.5, 8), ('N', 0.30000001192092896, 14),
       ('N', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

Такое представление имеет тот же тип данных и размер, что и индексированное поле исходного массива, поэтому представление - это неструктурированный массив. Но в случае вложенных структур это не так:

dots = np.array([('A', 0.5, ('a1', 12)),
                 ('B', 0.2, ('b1', 17)),
                 ('C', 0.19, ('c1', 21)),
                 ('D', 0.11, ('d1', 27))],
        dtype = [('char', 'U1'),
                 ('probability', 'f2'),
                 ('coordinate', [('pseudonyme', 'U2'),
                                 ('value', 'i4')])])
>>>
>>> dots
array([('A', 0.5, ('a1', 12)), ('B', 0.199951171875, ('b1', 17)),
       ('C', 0.18994140625, ('c1', 21)), ('D', 0.1099853515625, ('d1', 27))], 
      dtype=[('char', '<U1'), ('probability', '<f2'),
      ('coordinate', [('pseudonyme', '<U2'), ('value', '<i4')])])
>>>
>>> dots['coordinate']    #  Имеет структурированный тип данных
array([('a1', 12), ('b1', 17), ('c1', 21), ('d1', 27)], 
      dtype=[('pseudonyme', '<U2'), ('value', '<i4')])
>>> 
>>> dots['coordinate']['pseudonyme']    #  Имеет простой тип данных...
array(['a1', 'b1', 'c1', 'd1'], 
      dtype='<U2')
>>>
>>> dots['coordinate']['value']
array([12, 17, 21, 27])
>>>
>>> #  Но просматривает те же данные что и dots
... dots['coordinate']['value'] = 111
>>> dots['coordinate']['value']
array([111, 111, 111, 111])
>>>
>>> dots
array([('A', 0.5, ('a1', 111)), ('B', 0.199951171875, ('b1', 111)),
       ('C', 0.18994140625, ('c1', 111)),
       ('D', 0.1099853515625, ('d1', 111))], 
      dtype=[('char', '<U1'), ('probability', '<f2'),
             ('coordinate', [('pseudonyme', '<U2'), ('value', '<i4')])])

13.4.2. Доступ к нескольким полям

Получить доступ к нескольким полям структурированного массива можно если указать в качестве индекса список имен полей:

>>> probabilities = np.array([('A',0.5, 8),
                              ('B', 0.3, 14),
                              ('C', 0.2, 21)],
                     dtype = {'char':('U1', 0),
                              'probability':('f4', 4),
                              'value':('i2', 8)})
>>> 
>>> probabilities[['char', 'value']]
array([('A', 8), ('B', 14), ('C', 21)], 
      dtype=[('char', '<U1'), ('value', '<i2')])
>>> 

Результирующий массив, так же как и при однополевой индексации, является представлением исходного массива, т.е. имеет с ним общие данные:

>>> probabilities[['char', 'value']] = ('x', 0)
>>> probabilities[['char', 'value']]
array([('x', 0), ('x', 0), ('x', 0)],
      dtype={'names':['char','value'], 'formats':['<U1','<i2'], 'offsets':[0,8], 'itemsize':10})
>>> 
>>> probabilities[['char', 'value']] = [('y', 32), ('y', 42), ('y', 81)]
>>> probabilities[['char', 'value']]
array([('y', 32), ('y', 42), ('y', 81)],
      dtype={'names':['char','value'], 'formats':['<U1','<i2'], 'offsets':[0,8], 'itemsize':10})
>>>
>>> probabilities[['char','value']] = np.array([('z', 0), ('z', 0), ('z', 0)],
                                                dtype=[('char', 'U1'), ('value', 'i2')])
>>> probabilities[['char', 'value']]
array([('z', 0), ('z', 0), ('z', 0)],
      dtype={'names':['char','value'], 'formats':['<U1','<i2'], 'offsets':[0,8], 'itemsize':10})

Поменять поля местами не получится, только данные полей, и то только если их типы либо одинаковы либо совместимы:

>>> probabilities = np.array([('A',0.5, 8),
                                  ('B', 0.3, 14),
                                  ('C', 0.2, 21)],
                         dtype = {'char':('U1', 0),
                                  'probability':('f4', 4),
                                  'value':('i2', 8)})
>>> 
>>> probabilities[['char','value']] = probabilities[['value','char']]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'A'
>>> 
>>> probabilities[['probability','value']] = probabilities[['value','probability']]
>>> probabilities
array([('A',  8., 0), ('B', 14., 0), ('C', 21., 0)],
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

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


13.4.3. Индексация целыми числами

Индексирование структурированного массива одним числом возвращает структурированную строку исходного массива:

>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])
>>>
>>> probabilities[0]
('A', 0.5, 8)
>>>
>>> probabilities[1]
('B', 0.30000001192092896, 14)
>>>
>>> probabilities[2]
('C', 0.20000000298023224, 21)

Такие строки являются представлениями исходного массива, они изменяемы и поддерживают индексацию не только целыми числами, но и именами полей исходного структурированного массива:

>>> probabilities
array([('A', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])
>>>
>>> probabilities[0][0]
'A'
>>> probabilities[0]['char']
'A'
>>>
>>> probabilities[0]['char'] = 'D'
>>> probabilities
array([('D', 0.5, 8), ('B', 0.30000001192092896, 14),
       ('C', 0.20000000298023224, 21)], 
      dtype=[('char', '<U1'), ('probability', '<f4'), ('value', '<i2')])

Индексация целыми числами так же позволяет использовать срезы и задавть шаг элементов:

>>> b = np.array([('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1), ('f', 1), ('g', 1)],
                  dtype=[('char', 'U1'), ('b', 'i4')])
>>> b
array([('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1), ('f', 1), ('g', 1)], 
      dtype=[('char', '<U1'), ('b', '<i4')])
>>>
>>> b[:]
array([('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1), ('f', 1), ('g', 1)], 
      dtype=[('char', '<U1'), ('b', '<i4')])
>>> 
>>> b[3:]
array([('d', 1), ('e', 1), ('f', 1), ('g', 1)], 
      dtype=[('char', '<U1'), ('b', '<i4')])
>>>
>>> b[:3]
array([('a', 1), ('b', 1), ('c', 1)], 
      dtype=[('char', '<U1'), ('b', '<i4')])
>>> 
>>> b[0:7:2]
array([('a', 1), ('c', 1), ('e', 1), ('g', 1)], 
      dtype=[('char', '<U1'), ('b', '<i4')])