6.6. Indeksowanie i wycinanie

ndarray adresuje się na cztery sposoby: pojedynczymi indeksami, wycinkami, maskami logicznymi oraz formami przypisania każdego z nich.

6.6.1. Pojedyncze elementy

Indeksowanie w nawiasach kwadratowych zwraca wartość na podanej pozycji:

a = np.arange(10, dtype=np.uint8)
print(a[0], a[-1])      # 0 9
print(a[1], a[-2])      # 1 8

Indeksy ujemne liczą się od końca, tak samo jak w przypadku listy Pythona list. Indeks poza zakresem zgłasza IndexError.

W przypadku tablic o wyższym rzędzie każda oś przyjmuje indeks. Indeksy umieszcza się wewnątrz jednego zestawu nawiasów, oddzielone przecinkami:

m = np.arange(9, dtype=np.uint8).reshape((3, 3))
print(m[1, 1])          # 4
print(m[2, 0])          # 6

Gdy podana jest mniejsza liczba indeksów niż osi, nieindeksowane osie pozostają nienaruszone. Wynikiem jest widok o zredukowanym rzędzie źródła:

print(m[0])             # the first row, as a 1-D view of m

6.6.2. Wycinki

Wycinek start:stop:step zwraca widok tablicy. Widok współdzieli bazowy bufor danych ze źródłem; zapis przez widok zapisuje do źródła:

a = np.arange(10, dtype=np.uint8)
v = a[::2]              # array([0, 2, 4, 6, 8], dtype=uint8)
v[0] = 99
print(a)
# array([99, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

Gdy potrzebny jest niezależny bufor, copy() tworzy go jawnie.

Wycinanie rozszerza się naturalnie na wyższe wymiary. Każda oś przyjmuje własny wycinek:

m = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]], dtype=np.uint8)

m[0]            # first row
m[0, :2]        # first two elements of row 0
m[:, 0]         # column 0 (still 2-D in ulab)
m[-1]           # last row
m[::2, ::2]     # every other row, every other column

Mieszanie liczby całkowitej (pojedynczy indeks, usuwa oś) i wycinka (zachowuje oś) jest dozwolone i jest to sposób, w jaki zwykle zapisuje się dostęp do pojedynczego wiersza / pojedynczej kolumny.

6.6.3. Maski logiczne

Tablica logiczna o tym samym kształcie co źródło wybiera elementy tam, gdzie maska ma wartość True. Indeksowanie logiczne działa obecnie na tablicach 1-D; wejścia o wyższym rzędzie zgłaszają NotImplementedError

a = np.arange(9, dtype=np.float)
mask = a < 5
print(a[mask])

Wyjście:

array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)

Maska jest zwykłą tablicą bool ndarray, więc działa każde wyrażenie, które ją zwraca:

b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
a = np.arange(9, dtype=np.uint8)
print(a[a * a > np.sin(b) * 100.0])

Indeksowanie logiczne zwraca kopię. Wybrane elementy leżą na dowolnych pozycjach, w których maska ma wartość True – a nie w regularnym kroku przez źródło – więc nie istnieje deskryptor, którego mógłby użyć widok, aby je zaadresować, i wynik jest materializowany do własnego bufora.

6.6.4. Indeksowanie tablicą liczb całkowitych

Przekazanie listy lub tablicy indeksów w nawiasach wybiera te elementy w jednym kroku:

a = np.array([10, 20, 30, 40, 50], dtype=np.uint8)
a[[0, 2, 4]]
# array([10, 30, 50], dtype=uint8)

Wynikiem jest kopia; wybrane elementy nie współdzielą już pamięci ze źródłem. Ta sama forma działa po lewej stronie przypisania:

a[[0, 2, 4]] = 0
# array([0, 20, 0, 40, 0], dtype=uint8)

take() (omówione na Wybór i porządkowanie) jest funkcyjną formą tej samej operacji i akceptuje słowo kluczowe out= do użycia bez alokacji w pętli strumieniowej.

6.6.5. Przypisanie do wycinka

Wycinki i maski pojawiają się po lewej stronie przypisania, podobnie jak po prawej. Prawa strona może być skalarem, inną tablicą lub widokiem:

m = np.zeros((3, 3), dtype=np.uint8)
m[0]      = 1            # whole row 0 set to 1
m[:, 2]   = 3            # whole column 2 set to 3
m[1, 1:3] = [7, 8]       # row 1, columns 1 and 2

Maski logiczne po lewej stronie zastępują elementy, które spełniają warunek:

a = np.arange(9, dtype=np.uint8)
a[a < 3] = 99
# array([99, 99, 99, 3, 4, 5, 6, 7, 8], dtype=uint8)

a = np.arange(9, dtype=np.uint8)
b = np.array(range(9)) + 12
a[b < 15] = b[b < 15]
# array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)

6.6.6. Dlaczego przypisanie do wycinka ma znaczenie na kamerze

Przypisanie do wycinka zapisuje przez już istniejącą tablicę. Nie jest alokowana żadna nowa tablica. To różnica między:

out = a + b              # makes a new array the size of a
out = out * 2            # makes another new array

a:

out[:] = a               # writes into the existing out
out   += b               # in place
out   *= 2               # in place

Pierwsza wersja prosi kamerę o RAM wart dwóch świeżych tablic; druga wersja nie prosi o nic. Na mikrokontrolerze z ograniczoną ilością RAM ta różnica jest często różnicą między skryptem, który działa komfortowo, a takim, któremu kończy się pamięć.

Wydajność omawia ten wzorzec szczegółowo. Na razie ważną regułą jest to, że przypisanie do wycinka, operatory arytmetyczne działające w miejscu (+=, *=, …) oraz słowo kluczowe out= funkcji uniwersalnych to trzy narzędzia, które umożliwiają aktualizacje bez alokacji.