6.6. 인덱싱과 슬라이싱

ndarray는 네 가지 방식으로 접근됩니다: 단일 인덱스, 슬라이스, 불리언 마스크, 그리고 각각의 할당 형태입니다.

6.6.1. 단일 요소

대괄호 인덱싱은 주어진 위치의 값을 반환합니다:

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

음수 인덱스는 Python list와 마찬가지로 끝에서부터 셉니다. 범위를 벗어난 인덱스는 IndexError를 발생시킵니다.

고차원 배열의 경우 모든 축이 인덱스를 받습니다. 인덱스는 하나의 대괄호 안에 콤마로 구분하여 넣습니다:

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

축의 개수보다 적은 인덱스가 제공되면 인덱싱되지 않은 축은 그대로 유지됩니다. 그 결과는 원본의 차원이 줄어든 뷰입니다:

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

6.6.2. 슬라이스

슬라이스 start:stop:step은 배열의 를 반환합니다. 뷰는 기저 데이터 버퍼를 원본과 공유하며, 뷰를 통해 기록하면 원본에 기록됩니다:

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)

독립적인 버퍼가 필요할 때는 copy()가 명시적으로 하나를 생성합니다.

슬라이싱은 고차원으로도 자연스럽게 확장됩니다. 각 축이 자체 슬라이스를 받습니다:

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

정수(단일 인덱스, 축을 제거함)와 슬라이스(축을 유지함)를 섞는 것이 허용되며, 단일 행 / 단일 열 접근이 보통 이렇게 작성됩니다.

6.6.3. 불리언 마스크

원본과 동일한 형태의 불리언 배열은 마스크가 True인 요소를 선택합니다. 불리언 인덱싱은 현재 1차원 배열에서 동작하며, 고차원 입력은 NotImplementedError를 발생시킵니다:

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

출력:

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

마스크는 평범한 bool ndarray이므로 이를 생성하는 모든 표현식이 동작합니다:

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])

불리언 인덱싱은 복사본을 반환합니다. 선택된 요소들은 마스크가 True인 임의의 위치에 놓여 있어 – 원본을 통한 규칙적인 스트라이드가 아니기 – 때문에 뷰가 이들을 주소 지정하는 데 사용할 수 있는 디스크립터가 존재하지 않으며, 결과는 자체 버퍼로 실체화됩니다.

6.6.4. 정수 배열 인덱싱

인덱스 리스트나 배열을 대괄호에 전달하면 해당 요소들을 한 단계로 골라냅니다:

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

결과는 복사본입니다. 골라낸 요소들은 더 이상 원본과 저장 공간을 공유하지 않습니다. 같은 형태가 할당의 왼쪽에서도 동작합니다:

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

take() (선택과 재배열에서 다룸)은 동일한 연산의 함수 형태로, 스트리밍 루프에서 할당 없이 사용할 수 있도록 out= 키워드를 받습니다.

6.6.5. 슬라이스 할당

슬라이스와 마스크는 할당의 오른쪽뿐 아니라 왼쪽에도 나타납니다. 오른쪽 항은 스칼라, 다른 배열, 또는 뷰일 수 있습니다:

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

왼쪽의 불리언 마스크는 조건을 만족하는 요소들을 교체합니다:

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. 카메라에서 슬라이스 할당이 중요한 이유

슬라이스 할당은 이미 존재하는 배열을 통해 기록합니다. 새 배열은 할당되지 않습니다. 그것이 다음의 차이입니다:

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

그리고:

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

첫 번째 버전은 카메라에 두 개의 새 배열 분량의 RAM을 요청합니다. 두 번째 버전은 아무것도 요청하지 않습니다. RAM이 제한된 마이크로컨트롤러에서 그 차이는 흔히 편안하게 실행되는 스크립트와 메모리가 부족해지는 스크립트의 차이가 됩니다.

성능에서 이 패턴을 자세히 다룹니다. 지금 중요한 규칙은 슬라이스 할당, 제자리 산술 연산자(+=, *=, …), 그리고 유니버설 함수의 out= 키워드가 할당 없는 업데이트를 가능하게 하는 세 가지 도구라는 것입니다.