Создание массивов: array, zeros, arange, linspace и другие

Урок — практический каталог конструкторов массивов NumPy и того, чем они отличаются и где какой уместен.

Конструктор массива — функция NumPy, создающая ndarray заданной формы и типа; выбор конструктора влияет и на удобство, и на производительность.

Из готовых данных: np.array

Самый прямой способ — обернуть существующий список или вложенные списки в np.array. Вложенность списков задаёт размерность: список чисел → 1D, список списков → 2D, и так далее. NumPy сам выводит dtype, но его можно задать явно.

import numpy as np

v = np.array([1, 2, 3])                       # 1D, dtype int64
m = np.array([[1, 2, 3], [4, 5, 6]])          # 2D, форма (2, 3)
f = np.array([1, 2, 3], dtype=np.float64)     # явный тип

print(v.shape, v.dtype)
print(m.shape, m.dtype)
print(f, f.dtype)

Вывод:

(3,) int64
(2, 3) int64
[1. 2. 3.] float64

Важно: чтобы получить 2D-массив, все «строки» должны быть одной длины. Если длины разные, в новых версиях NumPy это ошибка (раньше создавался «массив объектов» — почти всегда не то, что нужно).

Заполненные массивы: zeros, ones, full, empty

Часто нужен массив заданной формы, который мы потом заполним. Эти конструкторы принимают shape (кортеж) и создают данные сразу нужного размера — это куда эффективнее, чем наращивать массив по элементу.

import numpy as np

z = np.zeros((2, 3))            # все нули, float64 по умолчанию
o = np.ones((2, 3), dtype=int)  # все единицы, целые
c = np.full((2, 3), 7)          # заполнить константой 7
e = np.empty((2, 3))            # БЕЗ инициализации — мусор в памяти!

print(z)
print(o)
print(c)

Вывод:

[[0. 0. 0.]
 [0. 0. 0.]]
[[1 1 1]
 [1 1 1]]
[[7 7 7]
 [7 7 7]]

np.empty заслуживает отдельного предупреждения. Он выделяет память, но не заполняет её — в массиве будут случайные «мусорные» значения, оставшиеся от прежнего содержимого памяти. Он быстрее zeros, потому что пропускает шаг обнуления, но использовать его стоит только если вы гарантированно перезапишете все элементы сразу после создания. Иначе берите zeros.

Есть и парные «_like»-варианты: np.zeros_like(a), np.ones_like(a), np.full_like(a, 7) создают массив той же формы и типа, что a. Удобно, когда нужен «такой же, но пустой» буфер под результат.

Почему предвыделение вообще важно? Потому что у ndarray фиксированный размер, и «дорастить» его нельзя без полного перевыделения и копирования. Если вы знаете итоговый размер заранее (а в численных задачах это почти всегда так), правильный паттерн — создать буфер нужной формы один раз через zeros/empty и заполнять его по индексам. Антипаттерн — начинать с пустого массива и добавлять элементы по одному: каждый «append» копирует всё содержимое, и сложность становится квадратичной. Эту тему мы подробно разберём в разделе про память, но привычку «сначала выдели, потом заполняй» стоит закладывать с самого начала.

zeros или ones: имеет ли значение, чем заполнять

На первый взгляд разница между zeros и ones чисто косметическая. Но выбор начального значения иногда несёт смысл. Нулевой буфер удобен, когда вы будете накапливать сумму (нейтральный элемент сложения — ноль). Единичный — когда накапливаете произведение (нейтральный элемент умножения — единица). А np.full(shape, np.nan) с заполнением nan — отличный приём, когда нужно отличать «ещё не вычисленные» ячейки от настоящих нулей: после заполнения легко проверить через np.isnan, не осталось ли незаполненных позиций. Это маленькая, но полезная защита от логических ошибок.

Диапазоны: arange и linspace

Это два разных способа задать последовательность, и их постоянно путают.

np.arange(start, stop, step) — аналог встроенного range, но возвращает массив и допускает дробный шаг. Правый конец не включается. Вы задаёте шаг.

np.linspace(start, stop, num) — задаёт количество точек, равномерно распределённых от start до stop. По умолчанию правый конец включается. Вы задаёте число точек, а шаг считается автоматически.

import numpy as np

print(np.arange(0, 10, 2))        # шаг 2, до 10 не включая
print(np.arange(0, 1, 0.25))      # дробный шаг
print(np.linspace(0, 1, 5))       # 5 точек от 0 до 1 включительно

Вывод:

[0 2 4 6 8]
[0.   0.25 0.5  0.75]
[0.   0.25 0.5  0.75 1.  ]

Главное правило выбора: для целых индексов и счётчиков берите arange; для дробных сеток (например, значения функции на отрезке, оси графика) — linspace. У arange с дробным шагом есть коварная проблема: из-за ошибок округления чисел с плавающей точкой итоговое число элементов может оказаться на единицу больше или меньше ожидаемого. linspace от этого избавлен — вы прямо говорите, сколько точек хотите.

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

# arange: задаём ШАГ, конец не включаем
def arange(start, stop, step):
    out = []
    x = start
    while x < stop:
        out.append(round(x, 10))
        x += step
    return out

# linspace: задаём ЧИСЛО точек, конец включаем
def linspace(start, stop, num):
    if num == 1:
        return [start]
    step = (stop - start) / (num - 1)
    return [round(start + i * step, 10) for i in range(num)]

print("arange :", arange(0.0, 1.0, 0.25))
print("linspace:", linspace(0.0, 1.0, 5))

Вывод:

arange : [0.0, 0.25, 0.5, 0.75]
linspace: [0.0, 0.25, 0.5, 0.75, 1.0]

Видно, что при одинаковых краях arange дал 4 значения (без правого конца), а linspace — 5 (с правым концом). Это типичный источник путаницы.

Почему arange с дробями опасен: разбор

Стоит чуть глубже понять, почему дробный шаг в arange ненадёжен. Граница цикла проверяется как «пока текущее значение строго меньше stop». Текущее значение накапливается сложением шага: 0.0, 0.1, 0.2, .... Но 0.1 не представимо точно в двоичном float, поэтому накопленная сумма к десятому шагу может оказаться чуть-чуть меньше или чуть-чуть больше ожидаемого 1.0. Если она оказалась 0.9999999999 вместо 1.0, цикл сделает лишний шаг и добавит элемент; если 1.0000000001 — наоборот, остановится раньше. В итоге длина массива «пляшет» на единицу в зависимости от конкретных чисел. linspace избегает этого, потому что не накапливает шаг, а вычисляет каждую точку напрямую как start + i·(stop-start)/(num-1) и заранее знает точное число точек. Это и есть причина рекомендации «дробные сетки — через linspace».

Специальные конструкторы: eye, identity, diag, fromfunction

np.eye(n) и np.identity(n) создают единичную матрицу — нули с единицами на главной диагонали. np.diag либо извлекает диагональ из матрицы, либо строит диагональную матрицу из вектора. np.fromfunction заполняет массив, вызывая вашу функцию от индексов.

import numpy as np

print(np.eye(3))                         # единичная 3x3
print(np.diag([10, 20, 30]))             # диагональная из вектора

# fromfunction: значение = f(индексы). Здесь — таблица сложения
print(np.fromfunction(lambda i, j: i + j, (3, 3), dtype=int))

Вывод:

[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[[10  0  0]
 [ 0 20  0]
 [ 0  0 30]]
[[0 1 2]
 [1 2 3]
 [2 3 4]]

Идею fromfunction легко прочувствовать на Python: NumPy передаёт в вашу функцию массивы индексов, а не отдельные числа, и применяет её векторно. Аналог «по-питоновски»:

# Таблица сложения 3x3 через индексы
def build(rows, cols, f):
    return [[f(i, j) for j in range(cols)] for i in range(rows)]

table = build(3, 3, lambda i, j: i + j)
for row in table:
    print(row)

Вывод:

[0, 1, 2]
[1, 2, 3]
[2, 3, 4]

fromfunction: мощь, о которой забывают

fromfunction кажется экзотикой, но за ней стоит важная идея, пронизывающая весь NumPy: вместо того чтобы перебирать индексы циклом и для каждого считать значение, мы передаём в функцию сразу массивы индексов и получаем весь результат векторно. Это микромодель того, как вообще мыслят в NumPy — «операция над всеми индексами разом», а не «цикл по одному». Любую таблицу, значение которой зависит от позиции (координатная сетка, маска расстояний от центра, шахматный узор), можно построить через fromfunction без единого явного цикла. Когда вы научитесь видеть задачи в таком ключе, переход к полноценной векторизации в следующих разделах дастся легко.

Шпаргалка по выбору конструктора

ЗадачаКонструктор
Из готового спискаnp.array([...])
Нулевой буфер под результатnp.zeros(shape)
Заполнить константойnp.full(shape, value)
Целые индексы / счётчик с шагомnp.arange(start, stop, step)
Дробная равномерная сеткаnp.linspace(start, stop, num)
Единичная матрицаnp.eye(n)
Значение из индексовnp.fromfunction(f, shape)
«Такой же, но пустой»np.zeros_like(a)

Как выбрать конструктор: ход мысли

Когда конструкторов так много, помогает простая последовательность вопросов. Сначала: данные уже есть? Если да (список, кортеж, другой массив) — это np.array. Если нет, данные нужно сгенерировать. Тогда: это последовательность чисел? Если это индексы или счётчик с целым шагом — arange; если равномерная дробная сетка для оси или табулирования функции — linspace. Если же это просто буфер под будущий результат — важно ли начальное содержимое? Нужны нули — zeros, нужна константа — full, содержимое будет немедленно перезаписано полностью — можно empty ради скорости. Для матриц специальной структуры — eye (единичная), diag (диагональная). Эта цепочка покрывает почти все практические случаи и избавляет от привычки лепить всё через np.array со списками.

Отдельно отметим частую связку: создать «пустой» результат по образцу входа через np.zeros_like(x), а затем заполнить его в вычислении. Это и читаемо (видно, что форма и тип наследуются от x), и устойчиво к изменению входных данных — не нужно дублировать форму руками.

Подводные камни

  • np.empty без перезаписи. Он не обнуляет память — внутри мусор. Берите его только если сразу заполните весь массив.
  • arange с дробным шагом. Из-за округления длина результата непредсказуема. Для дробных сеток используйте linspace.
  • Списки разной длины в np.array. Это ошибка (или нежелательный массив объектов). Следите за прямоугольностью данных.
  • Наращивание массива в цикле. Сразу создавайте нужную форму через zeros/empty и заполняйте по индексам, а не «append по одному».

Лучшие практики

  • Под результат заранее известного размера выделяйте буфер один раз (zeros/empty), а не дописывайте по элементу.
  • Задавайте dtype явно, если важна точность или экономия памяти, — не полагайтесь только на автоопределение.
  • Для осей графиков и табулирования функций — linspace; для индексов — arange.

Итог

  • np.array — из готовых данных; форма определяется вложенностью списков.
  • zeros/ones/full/empty — буферы заданной формы; empty не инициализирует память.
  • arange задаёт шаг и не включает конец; linspace задаёт число точек и включает конец.
  • eye, diag, fromfunction — для матриц и заполнения по индексам.
Проверьте себя
1. Чем np.linspace(0, 1, 5) отличается от np.arange(0, 1, 0.25)?
AНичем, это синонимы
Blinspace задаёт число точек и включает правый конец, arange задаёт шаг и конец не включает
Carange всегда возвращает float, а linspace всегда int
Dlinspace работает только с целыми числами
2. Когда безопасно использовать np.empty(shape) вместо np.zeros(shape)?
AВсегда, empty просто быстрее и эквивалентен zeros
BТолько когда вы сразу же перезапишете все элементы массива
CКогда нужен массив, гарантированно заполненный нулями
DТолько для одномерных массивов
3. Почему для дробной равномерной сетки предпочтительнее linspace, а не arange?
Aarange не умеет работать с дробями вовсе
BУ arange с дробным шагом из-за округления число элементов может оказаться непредсказуемым
Clinspace быстрее в сотни раз
Darange всегда включает оба конца и путает границы
Поддержать проект