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-вимірною, тому це обмеження рідко є проблемою.