6.5. Форма та кроки

Дані всередині ndarray – це один упакований блок чисел. Дескриптор перед цим блоком визначає, як цей плаский блок читається як тензор.

6.5.1. Що записує дескриптор

П’ять значень описують, як читати блок даних як тензор:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)

a.ndim       # 2     - number of dimensions
a.shape      # (2, 3)- length along each dimension
a.itemsize   # 1     - bytes per element (from dtype)
a.size       # 6     - total number of elements
a.strides    # (3, 1)- step pattern through the buffer

Допоміжна функція ndinfo() виводить усі їх, а також розташування базового буфера за один виклик. Два масиви, розташування буферів яких збігаються, спільно використовують пам’ять:

np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8

6.5.2. Пояснення кроків

Крок – це кількість байт, на яку потрібно просунутися у блоці даних, щоб перемістити один елемент вздовж заданої осі. Для масиву uint8 розміром 2x3 кроки дорівнюють (3, 1): переміщення на один рядок вниз стрибає на 3 байти, переміщення на один стовпець вправо – на 1 байт. Це те саме, що сказати, що рядки зберігаються один за одним, зліва направо:

memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
          ^ row 0          ^ row 1
          <------- 3 bytes ---->

Для читання a[i, j] numpy обчислює i * strides[0] + j * strides[1] від початку блоку даних і читає звідти itemsize байт. Та сама формула поширюється на будь-яку кількість вимірів.

Це розміщення – рядки зберігаються один за одним, з найшвидшою зміною по останній осі вздовж пам’яті – називається порядком «рядок за рядком». Кожен масив, який numpy виділяє на камері, використовує це розміщення.

6.5.3. Порядок «рядок за рядком» має наслідки

Із «рядків, що зберігаються один за одним» випливають дві речі, які важливі при формуванні буфера на камері.

Остання вісь є суміжною. Перехід від a[0, 0] до a[0, 1] торкається наступного байту. Перехід від a[0, 0] до a[1, 0] перестрибує через цілий рядок.

Остання вісь є швидкою для математики над цілим масивом. numpy на камері завжди обходить останню вісь у внутрішньому циклі, незалежно від того, яка вісь є довшою. Бібліотека numpy для комп’ютера мовчки переупорядковує свої цикли, щоб помістити найдовшу вісь у внутрішній цикл; камера цього не робить, тому вибір розміщення, який numpy для комп’ютера нівелював би, тут усе ще коштує часу. np.sum(m, axis=1) згортає останню вісь і виконується у суміжному напрямку; np.sum(m, axis=0) – ні. Якщо у застосунку є вибір, як розмістити буфер, розміщуйте довгу вісь останньою, щоб операції з нею залишалися у внутрішньому циклі.

Якщо розміщення спочатку невірне, transpose() (або скорочення .T) виправляє це без копіювання даних – просто міняє місцями кроки:

a = b.T            # now iterates fast

Продуктивність містить повне обговорення продуктивності.

6.5.4. Reshape, transpose, зрізи – редагування дескриптора

Будь-яка операція, що лише переписує дескриптор, є безкоштовною. reshape замінює нові shape та strides у тому самому блоці даних. transpose обертає кроки. a[::2] подвоює крок. Кожна повертає представлення того самого базового буфера.

Усе, що повинно обходити дані та записувати новий буфер, є копією. Правило наразі таке: редагування дескриптора безкоштовне, а обходи даних – ні.

6.5.5. Примітка про ndim

numpy на камері побудований з максимально підтримуваним ndim рівним 4. Операції, що призвели б до масиву вищого рангу, викликають ValueError. Переважна більшість роботи на стороні камери є 1-вимірною або 2-вимірною, тому це обмеження рідко є проблемою.