Структурированные массивы в 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')]

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