Ответ 1
Фактические данные массива Numpy хранятся в однородном и непрерывном блоке памяти, называемом буфером данных. Для получения дополнительной информации см. Внутренности NumPy. Используя (по умолчанию) основной порядок строк, 2D-массив выглядит следующим образом:
Чтобы отобразить индексы i, j, k,... многомерного массива на позиции в буфере данных (смещение в байтах), NumPy использует понятие шагов. Шагов - это количество байтов, которые нужно перепрыгнуть в памяти, чтобы перейти от одного элемента к следующему элементу по каждому направлению/измерению массива. Другими словами, это разделение байтов между последовательными элементами для каждого измерения.
Например:
>>> a = np.arange(1,10).reshape(3,3)
>>> a
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
Этот двумерный массив имеет два направления: оси-0 (проходящие вертикально вниз по рядам) и ось-1 (проходящие горизонтально между столбцами), причем каждый элемент имеет размер:
>>> a.itemsize # in bytes
4
Таким образом, чтобы перейти от a[0, 0] → a[0, 1]
(двигаясь горизонтально вдоль 0-й строки, от 0-го столбца до 1-го столбца), шаг байта в буфере данных равен 4. То же самое для a[0, 1] → a[0, 2]
, a[1, 0] → a[1, 1]
и т.д. Это означает, что количество шагов для горизонтального направления (ось-1) составляет 4 байта.
Однако, чтобы перейти от a[0, 0] → a[1, 0]
(двигаясь по вертикали вдоль 0-го столбца, от 0-й строки к 1-й строке), вам необходимо сначала пройти все оставшиеся элементы в 0-й строке. чтобы добраться до 1-й строки, а затем пройти через 1-ю строку, чтобы добраться до элемента a[1, 0]
, то есть a[0, 0] → a[0, 1] → a[0, 2] → a[1, 0]
. Поэтому число шагов для вертикального направления (ось-0) составляет 3 * 4 = 12 байтов. Обратите внимание, что переход от a[0, 2] → a[1, 0]
, и в целом от последнего элемента строки я -th к первому элементу строки (i + 1) -th, равен также 4 байта, потому что массив a
хранится в главном порядке строк.
Вот почему
>>> a.strides # (strides[0], strides[1])
(12, 4)
Вот еще один пример, показывающий, что шаги в горизонтальном направлении (ось-1), strides[1]
, двумерного массива необязательно равны размеру элемента (например, массив с основным порядком столбцов):
>>> b = np.array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]]).T
>>> b
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
>>> b.strides
(4, 12)
Здесь strides[1]
кратен размеру предмета. Хотя массив b
выглядит идентично массиву a
, он представляет собой другой массив: внутри b
хранится как |1|4|7|2|5|8|3|6|9|
(потому что транспонирование не влияет на буфер данных, а только меняет шаги и форму), тогда как a
as |1|2|3|4|5|6|7|8|9|
, Что делает их похожими, так это разные шаги. То есть шаг байта для b[0, 0] → b[0, 1]
составляет 3 * 4 = 12 байт, а для b[0, 0] → b[1, 0]
- 4 байта, тогда как для a[0, 0] → a[0, 1]
- 4 байта, а для a[0, 0] → a[1, 0]
- 12 байтов.
И последнее, но не менее важное: NumPy позволяет создавать виды существующих массивов с возможностью изменения шагов и формы, см. Трюки с шагами. Например:
>>> np.lib.stride_tricks.as_strided(a, shape=a.shape[::-1], strides=a.strides[::-1])
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
что эквивалентно транспонированию массива a
.
Позвольте мне добавить, но не вдаваясь в подробности, что можно даже определить шаги, которые не кратны размеру предмета. Вот пример:
>>> a = np.lib.stride_tricks.as_strided(np.array([1, 512, 0, 3], dtype=np.int16),
shape=(3,), strides=(3,))
>>> a
array([1, 2, 3], dtype=int16)
>>> a.strides[0]
3
>>> a.itemsize
2