6.5. Kształt i kroki

Dane wewnątrz ndarray są jednym upakowanym blokiem liczb. Deskryptor poprzedzający ten blok decyduje, jak ten płaski blok jest odczytywany jako tensor.

6.5.1. Co zapisuje deskryptor

Pięć wartości opisuje, jak odczytać blok danych jako tensor:

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

Funkcja pomocnicza ndinfo() wypisuje je wszystkie wraz z lokalizacją bazowego bufora w jednym wywołaniu. Dwie tablice, których lokalizacje buforów się zgadzają, współdzielą pamięć:

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

6.5.2. Wyjaśnienie kroków

Krok (stride) to liczba bajtów, o jaką należy przesunąć się w bloku danych, aby przesunąć się o jeden element wzdłuż danej osi. Dla powyższej tablicy 2x3 uint8 kroki wynoszą (3, 1): przesunięcie w dół o jeden wiersz skacze o 3 bajty, przesunięcie w prawo o jedną kolumnę skacze o 1 bajt. To to samo, co stwierdzenie, że wiersze są przechowywane jeden za drugim, od lewej do prawej:

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

Aby odczytać a[i, j], numpy oblicza i * strides[0] + j * strides[1] od początku bloku danych i odczytuje stamtąd itemsize bajtów. Ten sam wzór rozszerza się na dowolną liczbę wymiarów.

Ten układ – wiersze przechowywane jeden za drugim, z ostatnią osią zmieniającą się najszybciej wzdłuż pamięci – nazywany jest porządkiem wierszowym (row-major). Każda tablica, którą numpy alokuje na kamerze, używa tego układu.

6.5.3. Porządek wierszowy ma konsekwencje

Z „wierszy przechowywanych jeden za drugim” wynikają dwie rzeczy, które mają znaczenie podczas kształtowania bufora na kamerze.

Ostatnia oś jest ciągła. Przejście od a[0, 0] do a[0, 1] dotyka kolejnego bajtu. Przejście od a[0, 0] do a[1, 0] przeskakuje przez cały wiersz.

Ostatnia oś jest szybką osią dla obliczeń na całych tablicach. numpy na kamerze zawsze przechodzi ostatnią oś najbardziej wewnętrznie, niezależnie od tego, która oś jest dłuższa. Desktopowa biblioteka numpy po cichu zmienia kolejność swoich pętli, aby umieścić najdłuższą oś najbardziej wewnętrznie; kamera tego nie robi, więc wybór układu, który desktopowy numpy by zatuszował, tutaj nadal kosztuje czas. np.sum(m, axis=1) zwija ostatnią oś i działa w kierunku ciągłym; np.sum(m, axis=0) tego nie robi. Gdy aplikacja ma wybór co do sposobu ułożenia bufora, umieść długą oś na końcu, aby operacje wzdłuż niej pozostały w pętli wewnętrznej.

Jeśli układ jest początkowo błędny, transpose() (lub skrót .T) naprawia go bez kopiowania danych – po prostu zamienia kroki:

a = b.T            # now iterates fast

Wydajność zawiera pełne omówienie wydajności.

6.5.4. Reshape, transpose, wycinanie – edycje deskryptora

Każda operacja, która jedynie przepisuje deskryptor, jest darmowa. reshape zamienia nowy shape i strides na tym samym bloku danych. transpose odwraca kroki. a[::2] podwaja krok. Każda zwraca widok tego samego bazowego bufora.

Wszystko, co musi przejść przez dane i zapisać nowy bufor, jest kopią. Na razie reguła brzmi, że edycje deskryptora są darmowe, a przejścia przez dane nie.

6.5.5. Uwaga o ndim

numpy na kamerze jest zbudowany z maksymalnym obsługiwanym ndim równym 4. Operacje, które dałyby tablicę o wyższym rzędzie, zgłaszają ValueError. Zdecydowana większość pracy po stronie kamery jest 1-D lub 2-D, więc limit ten rzadko stanowi problem.