Learning Platform
Глоссарий Troubleshooting
Урок 06.04 · 28 мин
Начальный
numpyndarraydtypestridecontiguous

Чем ndarray отличается от list

numpy.ndarray — это настоящий массив в смысле модуля 4. Под капотом — непрерывный блок памяти, в котором лежат значения фиксированного размера, выбранного через dtype. Никаких PyObject-обёрток на каждое значение. Доступ по индексу — арифметика адресов, как в C.

import numpy as np

arr = np.array([10, 20, 30, 40, 50], dtype='int32')
print(arr.dtype)         # int32
print(arr.itemsize)      # 4 байта
print(arr.nbytes)        # 5 * 4 = 20 байт буфера
print(arr.data)          # <memory at 0x...>

itemsize * len(arr) = nbytes — плотная упаковка. На 1М int32 = 4 МБ, ровно. Сравните с list[int] на 1М ~36 МБ (из урока 02-python-list-internals).

Anatomy of ndarray: dtype + shape + strides

Главные поля ndarray:

  • dtype — тип значений в буфере. Определяет itemsize и интерпретацию байт. Примеры: int8 (1 байт), int32 (4), int64 (8), float64 (8), bool (1).
  • shape — tuple размерностей. (1000,) для 1D из 1000 элементов; (100, 50) для 100x50 матрицы.
  • strides — tuple шагов в байтах между соседними элементами по каждой оси.
  • data — указатель на буфер.

shape и strides вместе позволяют ndarray быть многомерным без копирования: транспонирование, срез, reshape часто меняют только метаданные, а буфер остаётся тот же.

2D ndarray: shape (3,4), dtype int32

Буфер 48 байт, стрелки strides показывают как двигаться по строкам/столбцам.

[0,0]=10ofs=0первый элемент в буфере
[0,1]=20ofs=4следующий int32
[0,2]=30ofs=8следующий
[0,3]=40ofs=12конец первой строки
[1,0]=50ofs=16вторая строка, скачок на 16 байт (4 * 4)
[1,1]=60ofs=20
[1,2]=70ofs=24
[1,3]=80ofs=28
strides=(16,4)row=16 byte, col=4 byteчтобы попасть на следующую строку — скачок 16 байт; на следующий столбец — 4 байта

arr[i, j] находится по формуле:

address = base + i * strides[0] + j * strides[1]

То же base + i * sizeof, что для 1D массива, просто на много измерений.

C-order vs F-order: row-major vs column-major

Когда буфер 2D, как разложить элементы линейно? Два варианта:

  • C-order (row-major) — строки идут подряд. [row0, row1, row2, ...]. Default в numpy.
  • F-order (column-major) — столбцы идут подряд. [col0, col1, col2, ...]. Default в Fortran, R, MATLAB.

Для 3x4 матрицы:

  • C-order: [10,20,30,40,50,60,70,80,90,100,110,120]
  • F-order: [10,50,90,20,60,100,30,70,110,40,80,120]

Разница важна для cache locality: проход по строкам в C-order работает с подряд лежащими байтами (хороший cache). Проход по столбцам в C-order делает скачки strides[0] байт — может быть cache-miss на каждом шаге.

import numpy as np
import time

n = 5000
arr = np.random.rand(n, n)  # C-order, float64

# проход по строкам (хороший cache)
t0 = time.perf_counter()
for i in range(n):
    _ = arr[i, :].sum()
t1 = time.perf_counter()
print(f"row scan:    {(t1-t0)*1000:.2f} ms")

# проход по столбцам (плохой cache в C-order)
t0 = time.perf_counter()
for j in range(n):
    _ = arr[:, j].sum()
t1 = time.perf_counter()
print(f"column scan: {(t1-t0)*1000:.2f} ms")

На обычном железе колоночный скан в C-order медленнее в 2-5 раз — это spatial locality в чистом виде.

Когда переходить с list на ndarray

Ситуации, в которых list плохо работает, а ndarray — хорошо:

  1. Численные данные больше 100k элементов. Память отличается в 4-10 раз, скорость batch-операций — в 30-100.
  2. Операции по всему массиву. sum, mean, std, dot product — все vectorized.
  3. Многомерные данные. Матрицы, тензоры. list of lists — это уже совсем плохо.
  4. Срезы без копирования. arr[100:200] в numpy это view (метаданные), без копирования буфера. В list — slice копирует указатели.
  5. Type stability. Один dtype, никакой динамической типизации. Это позволяет SIMD и JIT-compile через Numba.

Когда list остаётся правильным выбором:

  1. Маленькие коллекции (десятки-сотни элементов).
  2. Гетерогенные данные (разные типы в одном списке).
  3. Частые append/pop с обоих концов (используйте deque).
  4. Логика, не подаётся под vectorization (сложный if-else на каждом элементе).

pandas DataFrame как ndarray + метаданные

Когда у вас табличные данные (несколько колонок разных типов), правильная структура — pandas.DataFrame. Внутри это набор ndarray (один на колонку или несколько колонок одного dtype в «блоке»). Все преимущества ndarray (плотность, vectorized) + табличная семантика (groupby, join, filter).

import pandas as pd

df = pd.DataFrame({
    'user_id': np.arange(1_000_000, dtype='int64'),
    'amount':  np.random.rand(1_000_000) * 100,
    'country': np.random.choice(['RU', 'US', 'CN', 'DE'], 1_000_000),
})

print(df.dtypes)
print(df.memory_usage(deep=True).sum() / 1e6, "MB")

DataFrame с 1М записей часто занимает 20-50 МБ. Эквивалентный list of dicts ([{...}, {...}, ...]) был бы 200-500 МБ.

Бенчмарк: dot product

import time
import numpy as np

n = 1_000_000

# list версии
a_list = [i / n for i in range(n)]
b_list = [(n - i) / n for i in range(n)]

t0 = time.perf_counter()
result = sum(a * b for a, b in zip(a_list, b_list))
t1 = time.perf_counter()
list_ms = (t1 - t0) * 1000

# numpy версии
a_arr = np.array(a_list, dtype='float64')
b_arr = np.array(b_list, dtype='float64')

t0 = time.perf_counter()
result = np.dot(a_arr, b_arr)
t1 = time.perf_counter()
numpy_ms = (t1 - t0) * 1000

print(f"list zip+sum: {list_ms:.2f} ms")
print(f"numpy.dot:    {numpy_ms:.2f} ms")
print(f"speedup:      {list_ms / numpy_ms:.1f}x")

Типовой результат:

list zip+sum: 75.50 ms
numpy.dot:    0.50 ms
speedup:      151.0x

В 150 раз. Это не разница в алгоритме (оба O(n)). Это разница в константе: native C + SIMD vs Python loop с PyObject. Это и есть главный аргумент в пользу numpy.

Подводный камень: copy vs view

Срез ndarray возвращает view — указатель на тот же буфер с другими metadata. Изменение view меняет оригинал:

arr = np.array([1, 2, 3, 4, 5])
view = arr[1:4]
view[0] = 999
print(arr)   # [1, 999, 3, 4, 5]

Это эффективно (без копирования), но опасно (неожиданная мутация). Если нужна копия — view = arr[1:4].copy().

В list срез копирует:

xs = [1, 2, 3, 4, 5]
sl = xs[1:4]
sl[0] = 999
print(xs)    # [1, 2, 3, 4, 5] — не изменилось

Это разница семантики, которую надо помнить при переходе с list на ndarray.

Попробуй сам

Сравните память и скорость на одной задаче — посчитать среднее:

import sys
import time
import numpy as np

N = 5_000_000

# list[float]
lst = [i * 0.1 for i in range(N)]
list_bytes = sys.getsizeof(lst) + N * sys.getsizeof(0.1)

t0 = time.perf_counter()
mean_lst = sum(lst) / len(lst)
t1 = time.perf_counter()
list_ms = (t1 - t0) * 1000

# numpy.ndarray
arr = np.arange(N, dtype='float64') * 0.1
arr_bytes = arr.nbytes

t0 = time.perf_counter()
mean_arr = arr.mean()
t1 = time.perf_counter()
numpy_ms = (t1 - t0) * 1000

print(f"list:  {list_bytes / 1e6:>7.1f} MB  {list_ms:>7.2f} ms")
print(f"numpy: {arr_bytes / 1e6:>7.1f} MB  {numpy_ms:>7.2f} ms")
print(f"memory ratio: {list_bytes / arr_bytes:.1f}x")
print(f"speed ratio:  {list_ms / numpy_ms:.1f}x")

Скорее всего, в среднем 4x экономия памяти и 100-300x ускорение. Это пороги принятия решения «list или numpy» для DE-задач.

pandas DataFrame: eager evaluation и copy-on-write Polars: lazy API и Apache Arrow backend
Проверка знанийKnowledge check
У вас 2D-матрица numpy 10000x10000 float64 в C-order (default). Вы хотите посчитать сумму каждого столбца. Вариант A: цикл for j in range(10000): sum += arr[:, j].sum(). Вариант B: arr.sum(axis=0). Почему B в десятки раз быстрее, помимо того, что это nativeC?
ОтветAnswer
У A две проблемы: (1) Python-loop на 10000 итераций — overhead интерпретатора; (2) каждое arr[:, j] делает column-scan в C-order — это плохая spatial locality, потому что соседние элементы столбца лежат через 10000*8 = 80000 байт (stride), а cache line всего 64 байта. CPU читает много линий и большая часть данных не используется. arr.sum(axis=0) в native C делает один проход по буферу в row-order: 100M элементов идут подряд, prefetcher работает идеально, SIMD читает 4 float64 за такт. Кроме отсутствия Python-overhead, главная экономия — за счёт cache-friendly прохода по C-order буферу.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что хранит поле strides ndarray?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4