Структурированные массивы в NumPy
Создано: 26.11.2025
В конце прошлого урока было показано, что связанные данные может быть удобнее хранить в одном массиве, даже если он при этом становится двухмерным. Однако извлечение из вложенных массивов значений, находящиеся в одних и тех же позициях (например, координаты y), с помощью срезов не выглядит очевидно. Трудно понять, что имеется в виду в выражении points[:, 1]. Кроме того, при взгляде на массив нельзя сказать, что за данные он хранит.
Для решения подобных и других проблем в NumPy есть возможность создавать так называемые структурированные массивы (structured arrays). Они позволяют именовать поля, что, в свою очередь, дает возможность более очевидно извлекать соответствующие данные из всех вложенных массивов.
Однако в структурированный массив вложены не массивы, а некое подобие кортежей, и каждый из них называется записью. Таким образом, мы имеем дело с таблицей, где строки — это записи и индексируются как элементы обычного массива, а столбцы — поля, имеющие названия.
При создании структурированного массива в NumPy определяется свой структурированный тип данных. Для этого создается список из кортежей, каждый из которых описывает одно поле. Описание включает произвольное имя поля и задает тип для данных этого поля. Так в примере ниже (график будет таким же, как в конце прошлого урока) определяется структурированный тип, состоящий из двух целочисленных полей. Присваивать его заранее переменной не обязательно, определение может происходить в момент присваивания параметру dtype при создании массива.
import numpy as np import matplotlib.pyplot as plt coord_type = [('x', 'i1'), ('y', 'i2')] points = np.array([(1, 12), (3, 19), (5, 17), (6, 22), (8, 18), (9, 24)], dtype=coord_type) plt.plot(points['x'], points['y'], 'o-') plt.grid() plt.show()
Очевидно, что в определении структурированного типа данных описание каждого поля — это кортеж. Сам структурированный массив — ndarray. Однако каждый его элемент (запись) принадлежит типу numpy.void, и мы не можем извлекать отдельные значения из него так, как это делается со вложенными массивами, то есть через индексацию. Выражение points[0,1] вместо ожидаемого числа 12 вернет ошибку, так как структурированный массив является одномерным.
Вместо этого для получения значения какого-то поля из определенной записи используется синтаксис, в котором сначала в квадратных скобках указывается индекс записи, потом в отдельных квадратных скобках — имя поля:
points = np.array([(1, 12), (3, 19), (5, 17), (6, 22), (8, 18), (9, 24)], dtype=[('x', 'i1'), ('y', 'i2')]) print(points[0]) # (1, 12) print(points[0]['x']) # 1 p2 = points[1] print(p2['x'], p2['y']) # 3 19 print(p2[['x','y']]) # (3 19) print(points['x']) # [1 3 5 6 8 9] print(type(points[0])) # <class 'numpy.void'> print(type(points['x'])) # <class 'numpy.ndarray'> print(type(points)) # <class 'numpy.ndarray'>
Как и в любом другом массиве NumPy в структурированном можно менять значения элементов. При этом присваивание просто по имени поля заменит значение во всех записях.
points = np.array([(1, 12), (3, 19), (5, 17)], dtype=[('x', 'i1'), ('y', 'i2')]) points[0] = (100, 150) print(points) # [(100, 150) ( 3, 19) ( 5, 17)] points['y'] = 0 print(points) # [(100, 0) ( 3, 0) ( 5, 0)] points[1]['x'] = 55 print(points) # [(100, 0) ( 55, 0) ( 5, 0)]
Конечно, структурированный тип данных может быть более сложным, например, в качестве одного из своих полей включать другой структурированный тип:
grade_type = [('math', 'i1'), ('physics', 'i1'), ('chemistry', 'i1')] pupil_type = [('name', 'U20'), ('class', 'U3'), ('grades', grade_type)] school = np.array([('Tom Sawyer', '10A', (12, 14, 15)), ('Jack Ripper', '8B', (22, 34, 35))], dtype=pupil_type) for pupil in school: print(pupil['name'], end=': ') print(pupil['grades']['physics'])
Tom Sawyer: 14 Jack Ripper: 34
Для добавления новой записи в структурированный массив используется функция append библиотеки NumPy:
new_pupils = np.array([('Lora Green', '5A', (0, 0, 0))], dtype=pupil_type) school = np.append(school, new_pupils) print(school['name'])
['Tom Sawyer' 'Jack Ripper' 'Lora Green']
Понятно, что при этом создается новый структурированный массив, а не меняется имеющийся.
Если запись надо добавить не в начало или конец, то можно использовать срезы структурированного массива:
school = np.append(np.append(school[:1], new_pupils), school[1:]) print(school['name'])
['Tom Sawyer' 'Lora Green' 'Jack Ripper']
В подмодуле numpy.lib.recfunctions содержатся полезные функции для работы со структурированными массивами. Так добавить новое поле можно с помощью append_fields:
import numpy.lib.recfunctions as rfn ... g = np.array(['M', 'M', 'F'], dtype='U1') school = rfn.append_fields(school, 'gender', g) print(school)
[('Tom Sawyer', '10A', (12, 14, 15), 'M') ('Jack Ripper', '8B', (22, 34, 35), 'M') ('Lora Green', '5A', (0, 0, 0), 'F')]
Однако если нам потребуется добавить новое поле во вложенную структуру, то структурированные типы данных придется переопределить, добавляя поля в их определения. После этого скопировать данные, извлекая данные по столбцам из старого массива (по названиям полей) и присваивая их новым.