Skip to content

2. NumPy: как устроена библиотека и как мыслить массивами ​

Философия векторизации и архитектура библиотеки ​

Проблема «600 методов» и ментальная карта библиотеки ​

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

  1. Конструкторы (Constructors): Функции для создания данных с нуля (например, numpy.array, numpy.arange).
  2. Архитекторы формы (Shape Architects): Инструменты, меняющие геометрию и структуру массивов без изменения самих данных (например, .reshape(), транспонирование).
  3. Статистики-редукторы (Statistical Reducers): Функции, которые сжимают многомерные данные до агрегированных показателей (например, .sum(), .mean()).
  4. Векторизаторы (Vectorizers): Математические операции, применяемые ко всему массиву сразу.

Почему циклы в AI — это табу ​

В искусственном интеллекте веса нейросетей и датасеты состоят из миллионов чисел. Написание классического цикла for на Python для обработки таких объемов приводит к катастрофическому падению производительности. Цикл на Python вынужден динамически проверять типы данных на каждой итерации. Векторизация в NumPy переносит обход элементов на уровень скомпилированного C-кода, где тип данных известен заранее, а вычисления распараллеливаются на уровне процессора.

Универсальные функции (ufuncs) ​

В основе векторизации NumPy лежат универсальные функции - ufuncs. Это объекты-функции, которые выполняют поэлементные операции над массивами фиксированной размерности. Примерами ufuncs являются базовые арифметические операторы, а также тригонометрические и экспоненциальные функции. Главная особенность ufunc — поддержка механизма приведения типов и оптимизированный проход по непрерывным участкам памяти.

Анатомия и фундаментальные характеристики массива (ndarray) ​

Как устроен объект ndarray «под капотом» ​

Каждый массив NumPy — это не просто набор чисел, а структура данных на C, состоящая из двух частей: сырого блока памяти с данными (data buffer) и заголовка (metadata), описывающего, как эти данные интерпретировать. Из-за этого массив обладает жесткими физическими и геометрическими свойствами, которые критически важны для AI-инженера при расчете веса моделей.

Ключевые свойства (атрибутов) и характеристики массива ​

Для контроля структуры данных на практике используются следующие встроенные свойства (атрибуты) объекта numpy.ndarray:

1. Форма и геометрия (Shape & Geometry)

  • shape — размеры массива по осям (кортеж целых чисел). Используется перед любой математической операцией или передачей данных в нейросети, чтобы убедиться, что структуры данных совпадают и программа не упадет из-за несовместимости размеров (например, при умножении матриц).
  • ndim — количество измерений/осей массива. Помогает быстро понять структуру объекта в условиях if/else (1 — вектор, 2 — таблица, 3 — цветное RGB-изображение, 4 — пачка картинок).
  • size — общее количество элементов в массиве. В отличие от стандартного len(), который возвращает только количество строк, size честно считает абсолютно все ячейки. Полезно для контроля общего объема считанных данных.
  • strides — шаги в байтах, необходимые для перехода к следующему элементу по каждой оси. Показывает, как массив развернут в памяти компьютера. Новичку это редко нужно менять вручную, но именно благодаря этому свойству NumPy умеет мгновенно транспонировать матрицы без реального копирования чисел.
python
import numpy as np

# Initialize a 2D array (2 rows, 3 columns) of 32-bit integers
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)

print(f"Shape: {arr.shape}")      # Output: (2, 3) -> 2 rows, 3 columns
print(f"Dimensions: {arr.ndim}")  # Output: 2 -> two-dimensional array
print(f"Total size: {arr.size}")  # Output: 6 -> 6 elements in total
print(f"Strides: {arr.strides}")  # Output: (12, 4) -> 12 bytes to next row, 4 bytes to next column

2. Память и типы данных (Memory & Data Types)

  • dtype — тип данных элементов в массиве. Все элементы в NumPy должны быть одного типа. Проверка dtype критически важна при чтении данных из файлов: если числа случайно считались как текст (string), математика работать не будет.
  • itemsize — размер одного элемента массива в байтах. Помогает оценить "тяжесть" выбранного типа данных. Например, int8 занимает 1 байт, а float64 — 8 байт.
  • nbytes — суммарный размер всех элементов массива в байтах (size * itemsize). Позволяет точно узнать, сколько оперативной памяти (RAM) занимает ваш датасет. Если программа выдает ошибку MemoryError, проверка nbytes подскажет, нужно ли перевести массив на более легкий тип (например, с float64 на float32).
  • base — ссылка на базовый массив, если текущий является его «представлением» (view). Защищает от багов при создании срезов. Если base is not None, значит, текущий массив зависит от другого. Изменение элемента в нем испортит данные и в оригинальном массиве.
  • data — низкоуровневый буфер (указатель в памяти), где хранятся элементы массива. Прямой адрес первого элемента в оперативной памяти компьютера. Используется для низкоуровневой магии и интеграции на экспертном уровне.
python
import numpy as np

# Initialize a 2D array and create a view (slice)
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
view_arr = arr[0, :]  # Create a view of the first row

print(f"Data type: {arr.dtype}")     # Output: int32
print(f"Item size: {arr.itemsize}")  # Output: 4 -> since int32 takes 4 bytes
print(f"Total bytes: {arr.nbytes}")  # Output: 24 -> 6 elements * 4 bytes
print(f"View base: {view_arr.base}") # Output: [[1 2 3] [4 5 6]] -> points to the original array
print(f"Array base: {arr.base}")     # Output: None -> original array owns its data
print(f"Data pointer: {arr.data}")   # Output: <memory at 0x...> -> memory address

3. Комплексные числа (Complex Numbers)

  • real — массив, содержащий только вещественную (действительную) часть элементов. Извлекает чистые значения без мнимой компоненты. Применяется в продвинутом анализе сигналов, обработке аудио, радиоволн или при фильтрации изображений (Фурье-преобразования).
  • imag — массив, содержащий только мнимую часть элементов. Позволяет изолировать фазовые или мнимые характеристики комплексных чисел для их отдельного математического анализа в научных вычислениях.
python
import numpy as np

# Initialize an array of complex numbers
complex_arr = np.array([1 + 2j, 3 + 4j])

print(f"Real part: {complex_arr.real}")       # Output: [1. 3.]
print(f"Imaginary part: {complex_arr.imag}")  # Output: [2. 4.]

4. Представления и итераторы (Views & Iterators)

  • T — транспонированный массив (оси меняются местами). Поворачивает матрицу набок (строки становятся столбцами). Это базовая операция линейной алгебры, которая постоянно применяется при перемножении матриц и в алгоритмах Data Science / Machine Learning.
  • flat — плоский (1D) итератор по всем элементам массива. Позволяет обойти любой многомерный массив (даже 3D или 4D) в один простой цикл for без написания громоздких вложенных циклов.
  • ctypes — интерфейс для взаимодействия с функциями и библиотеками на языке C. Служит мостом для передачи данных из Python напрямую в готовые быстрые программы, написанные на C/C++, когда требуется максимальная оптимизация кода по скорости.
python
import numpy as np

# Initialize a 2D array
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)

# 1. Transpose the array (swap axes)
print("Transposed array:")
print(arr.T)  # Output: a 3x2 matrix

# 2. Iterate through multidimensional array as 1D without copying memory
print("Flat iteration:")
for element in arr.flat:
    print(element, end=' ')  # Output: 1 2 3 4 5 6
print()

# 3. Get C-compatible data pointer address
print(f"Ctypes data pointer: {arr.ctypes.data}")

Эволюция NumPy 2.0+ и Array API ​

Что навсегда устарело в NumPy 2.0+ ​

С релизом NumPy 2.0+ архитектура библиотеки была жестко очищена от устаревшего legacy-кода:

  • Удалены старые псевдонимы типов: Больше нельзя использовать np.float, np.int или np.bool. Теперь необходимо явно указывать стандартные типы Python (float, int, bool) или использовать точные низкоуровневые типы (например, numpy.float32, numpy.int64).
  • Полный отказ от numpy.matrix: Старый класс numpy.matrix (который всегда строго оставался двумерным) полностью удален. Вся работа с матрицами теперь ведется исключительно через стандартные многомерные массивы numpy.ndarray.

Вызов функции (numpy.sum(a)) против метода объекта (a.sum()) ​

В NumPy большинство операций можно вызвать двумя способами: через функцию модуля (numpy.sum(arr)) или как метод самого объекта (arr.sum()). Использование методов объекта делает цепочки вычислений более лаконичными, в то время как функции модуля полезны при работе в рамках строгого функционального программирования или стандартов Array API. Встроенные функцию Python, например sum() использовать с массивами NumPy не рекомендуется, ввиду более медленной скорости работы.

Упражнения базовые (5 шт) (только с опорой на объясненную в данном уроке теорию) ​

1. Полный аудит характеристик вектора ​

Задача: Создайте массив NumPy из списка [1.5, 2.5, 3.5, 4.5]. Напишите код, который последовательно выведет в консоль: сам массив, тип контейнера, тип элементов, размерность, форму (shape) и общее количество элементов.

Пример вывода:

text
Array:                   [1.5 2.5 3.5 4.5]
Container type:          <class 'numpy.ndarray'>
Elements type:           float64
Dimensions count:        1
Shape geometry:          (4,)
Total amount elements:   4
Посмотреть решение
python
import numpy

target_list = [1.5, 2.5, 3.5, 4.5]
features_array = numpy.array(target_list)

print("Array:", features_array)
print("Container type:", type(features_array))
print("Elements type:", features_array.dtype)
print("Dimensions count:", features_array.ndim)
print("Shape geometry:", features_array.shape)
print("Total size:", features_array.size)

2. Расчет физического веса и размера ячейки ​

Задача: Создайте массив целых чисел от 1 до 100 с помощью numpy.arange(1, 101). Выведите на экран два значения: сколько байт занимает одно число в этом массиве (.itemsize) и сколько байт весит весь массив целиком (.nbytes).

Посмотреть решение
python
import numpy

large_series = numpy.arange(1, 101)

# Inspecting memory meta-information
bytes_per_element = large_series.itemsize
total_bytes_weight = large_series.nbytes

print("Bytes per item:", bytes_per_element)
print("Total array bytes:", total_bytes_weight)

3. Сравнение способов вычисления суммы ​

Задача: Создайте массив из чисел [10, 20, 30]. Вычислите сумму его элементов двумя способами: с помощью функции из модуля numpy и с помощью метода самого объекта массива. Выведите оба результата, убедившись в их идентичности.

Посмотреть решение
python
import numpy

data_samples = numpy.array([10, 20, 30])

# Way 1: Global module function
sum_via_module = numpy.sum(data_samples)

# Way 2: Internal object method
sum_via_method = data_samples.sum()

print("Module sum:", sum_via_module)
print("Method sum:", sum_via_method)

4. Исследование шага памяти (Strides) одномерного массива ​

Задача: Создайте массив из чисел [1, 2, 3, 4], явно указав тип данных dtype=numpy.int32. Выведите значение атрибута .strides. Прочитайте в комментарии к решению, что означает полученное число.

Посмотреть решение
python
import numpy

# Explicit 32-bit integer array specification
strict_integers = numpy.array([1, 2, 3, 4], dtype=numpy.int32)

# Strides tells how many bytes to skip in RAM to move to the next item
print("Array strides:", strict_integers.strides)  # Outputs: (4,)
# Meaning: Since each int32 occupies exactly 4 bytes, the step to the next element is 4 bytes.

5. Логическая проверка ufunc-природы операции ​

Задача: Импортируйте numpy и выведите тип объекта универсальной функции умножения numpy.multiply с помощью встроенной функции type(). Убедитесь, что операция принадлежит к базовой архитектуре ufunc.

Посмотреть решение
python
import numpy

# Identifying core vectorized function components
math_operation_type = type(numpy.multiply)
print("Type of multiply operation:", math_operation_type)  # Outputs: <class 'numpy.ufunc'>