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