안녕하세요. 지난 포스팅의 넘파이 알고 쓰자 - 넘파이 연산에서는 넘파이 라이브러리가 제공하는 연산과 여러가지 범용함수에 대해서 알아보았습니다. 오늘은 파이썬을 공부하셨다면 다들 아실 인덱싱과 슬라이싱에 대해서 설명하도록 하겠습니다. 코드는 제 깃허브를 참고해주세요.
1. 파이썬에서의 인덱싱과 슬라이싱
파이썬에서는 반복가능한(iterable) 객체에 대해서 인덱싱과 슬라이싱을 지원하고 있습니다. 대표적인 반복가능한 객체로는 리스트(list)와 튜플(tuple)이 있습니다. 잠시 인덱싱, 슬라이싱을 복습하고 넘파이 객체에 적용하도록 하겠습니다.
1.1 인덱싱(Indexing)
# 파이썬 리스트 indexing
l = [1, 2, 3, 4, 5]
print("l[0] : ", l[0]) # 1
print("l[1] : ", l[1]) # 2
print("l[2] : ", l[2]) # 3
print("l[3] : ", l[3]) # 4
print("l[4] : ", l[4]) # 5
리스트를 인덱싱하는 코드입니다. 대부분의 프로그래밍 언어가 그렇지만 인덱스가 0부터 시작하는 것에 유의하시길 바랍니다. 첫번째 요소는 0번 인덱스, 두번째 요소는 1번 인덱스, ... 이런 식으로 진행됩니다. 만약, 값을 벗어난다면 어떻게 될까요?
print("l[5] : ", l[5])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-9-99464b2f4852> in <module>
----> 1 print("l[5] : ", l[5])
IndexError: list index out of range
보시는 것처럼 리스트의 6번째 값은 존재하지 않기 때문에 5번 인덱스를 가져오면 리스트의 인덱스 범위를 벗어난다는 IndexError를 보여주고 있습니다. 파이썬의 인덱싱의 재밌는 점은 음의 정수를 이용해서도 인덱싱을 할 수 있다는 것입니다. 음수라는 것을 직관적으로 받아들이면 뒤로 돌아간다는 느낌이 듭니다. 따라서 -1번 인덱스는 0번 인덱스의 뒤를 돌아서 마지막 인덱스를 의미하게 됩니다. -2번 인덱스는 마지막 인덱스에서 2번째 인덱스, -3번 인덱스는 마지막 인덱스에서 3번째 인덱스, ... 입니다. 이해가 잘 안되시면 아래의 코드를 보시면 쉽게 이해하실 수 있습니다.
print("l[-1] : ", l[-1]) # 5
print("l[-2] : ", l[-2]) # 4
print("l[-3] : ", l[-3]) # 3
print("l[-4] : ", l[-4]) # 2
print("l[-5] : ", l[-5]) # 1
그렇다면 -0번 인덱스는 무엇일까요?
print("l[-0] : ", l[-0]) # 1
print("l[0] : ", l[0]) # 1
여러분들이 생각하신것과 아마 일치할 것입니다. -0번 인덱스 = 0번 인덱스입니다. 아래의 코드를 보시면 튜플도 완전히 똑같은 것을 알 수 있습니다.
# 파이썬 튜플 indexing
t = (1, 2, 3, 4, 5)
print("t[0] : ", t[0]) # 1
print("t[1] : ", t[1]) # 2
print("t[2] : ", t[2]) # 3
print("t[3] : ", t[3]) # 4
print("t[4] : ", t[4]) # 5
print("t[5] : ", t[5])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-16-99028d27b346> in <module>
----> 1 print("t[5] : ", t[5])
IndexError: tuple index out of range
print("t[-1] : ", t[-1]) # 5
print("t[-2] : ", t[-2]) # 4
print("t[-3] : ", t[-3]) # 3
print("t[-4] : ", t[-4]) # 2
print("t[-5] : ", t[-5]) # 1
print("t[-0] : ", t[-0]) # 1
print("t[0] : ", t[0]) # 1
1.2 슬라이싱(Slicing)
인덱싱은 원하는 요소를 하나하나 뽑을 수 있는 방법입니다. 하지만, 만약 여러 개의 요소를 동시에 뽑고 싶다면 어떻게 해야할까요? 이때, 적용할 수 있는 것이 슬라이싱 입니다. 위의 리스트에서 1번째 인덱스부터 3번째 인덱스까지의 요소를 뽑고 싶다고 가정하겠습니다.
l[1:3] # [2, 3]
음... 저희가 원하는 결과는 2, 3, 4 인데 2, 3 까지만 나왔습니다. 그 이유는 슬라이싱을 하게 되면 뒤의 인덱스는 생략되기 때문입니다. 따라서 위의 코드는 1번째, 2번째 인덱스만 슬라이싱 한 것입니다. 3번째 인덱스까지 슬라이싱하고 싶다면 뒤의 인덱스를 4로 바꿔주면 됩니다!
l[1:4] # [2, 3, 4]
이렇게 저희가 원하는 값을 얻을 수 있습니다!! 슬라이싱은 아래와 같이 다양한 방법으로 쓰일 수 있습니다.
print("2번 인덱스부터 마지막 인덱스까지 = ", l[2:]) # [3, 4, 5]
print("첫번째 인덱스부터 2번 인덱스까지 = ", l[:3]) # [1, 2, 3]
print("모든 인덱스 = ", l[:]) # [1, 2, 3, 4, 5]
이런식으로 저희가 원하는 여러 개의 연속된 값을 아주 쉽게 얻을 수 있습니다.
또한, 리스트에서는 스텝 사이즈를 정하여 매 스텝에 해당하는 요소를 뽑을 수 있습니다.
print(l[1::2]) # [2, 4]
print(l[:4:2]) # [1, 3]
print(l[::2]) # [1, 3, 5]
첫번째 라인은 2번 인덱스부터 마지막 인덱스까지의 리스트 요소들 중 2번, 4번 요소만 선택한다는 것을 의미합니다. 두번째 라인은 1번 인덱스부터 3번째 인덱스까지의 리스트 요소들 중 1번, 3번 요소만 선택한다는 것을 의미합니다. 세번째 라인은 전체 리스트 요소들 중에서 1번, 3번, 5번 요소만 선택한다는 것을 의미합니다. 이 규칙을 이용하면 아래와 같이 간단하게 리스트를 뒤집을 수 있습니다.
l[::-1] # [5, 4, 3, 2, 1]
2. 넘파이에서의 인덱싱과 슬라이싱
방금까지 파이썬에서의 인덱싱과 슬라이싱을 간단하게 복습하였습니다. 이번에는 이를 넘파이 객체에 적용해보도록 하겠습니다. 넘파이 객체 역시 리스트, 튜플과 동일하게 인덱싱과 슬라이싱을 적용할 수 있는 객체이기 때문에 사용방법은 동일합니다.
a = np.array([1, 2 , 3, 4, 5])
print("a[0] : ", a[0]) # 1
print("a[1] : ", a[1]) # 2
print("a[2] : ", a[2]) # 3
print("a[3] : ", a[3]) # 4
print("a[4] : ", a[4]) # 5
print("a[-0] : ", a[-0]) # 1
print("a[-1] : ", a[-1]) # 5
print("a[-2] : ", a[-2]) # 4
print("a[-3] : ", a[-3]) # 3
print("a[-4] : ", a[-4]) # 2
또한 넘파이가 가진 크기를 넘어서는 인덱싱을 하려고 하면 동일한 오류를 보여줍니다.
print("a[6] = ", a[6])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-31-f7372c6db330> in <module>
----> 1 print("a[6] = ", a[6])
IndexError: index 6 is out of bounds for axis 0 with size 5
이렇게 보면 넘파이나 리스트나 크게 다를 것이 없습니다!! 그냥 둘 다 원하는 인덱스에 해당하는 정수를 넣어주면 얻을 수 있습니다. 또한, 슬라이싱 역시 동일하게 사용하므로 스텝 사이즈를 설정하여 원하는 인덱스만 골라서 뽑을 수 있습니다.
b = np.arange(1, 10)
print(b[2:7:2]) # [3 5 7]
2.1 다차원 넘파이 배열
이번에는 조금 더 어렵습니다! 방금까지의 예제는 1차원에서의 인덱싱과 슬라이싱을 하였지만 이번에는 2차원, 3차원과 같은 다차원 배열에서 진행하도록 하겠습니다. 사실 이 역시 다차원 리스트와 완전히 동일합니다. 일단 그나마 간단한 2차원 배열부터 보도록 하겠습니다. 2차원 배열은 아래와 같은 그림으로 표현할 수 있습니다.
세로는 행, 가로는 열이 됩니다. 이때, axis라는 개념이 도입되는 데 어려운 것은 아니고, 다차원이 되면서 그만큼 넘파이 객체의 ndim이 증가하게 됩니다. 이제 axis 역시 그만큼 증가한다고 보면 될 거 같습니다. 예를 들어서 벡터의 경우에는 ndim=1이기 때문에 axis=0만 존재합니다. 하지만 행렬의 경우에는 ndim=2이기 때문에 axis=0, 1로 2개의 축이 존재합니다. 만약, 3차원 텐서일 경우에는 ndim=3이기 때문에 axis=0, 1, 2로 3개의 축이 존재합니다. "축(axis)"라고 해서 어려운 개념이 아닌 단순하게 가로, 세로, 높이를 표현하는 방식이라고 보시면 됩니다. 넘파이 객체에서는 이를 아래와 같이 인덱싱합니다.
c = np.arange(1, 16).reshape(3, 5)
print(c)
# [[ 1 2 3 4 5]
# [ 6 7 8 9 10]
# [11 12 13 14 15]]
print("c[0, 0] = ", c[0, 0]) # 1
print("c[0, 1] = ", c[0, 1]) # 2
print("c[0, 2] = ", c[0, 2]) # 3
print("c[0, 3] = ", c[0, 3]) # 4
print("c[0, 4] = ", c[0, 4]) # 5
print("c[1, 0] = ", c[1, 0]) # 6
print("c[1, 1] = ", c[1, 1]) # 7
print("c[1, 2] = ", c[1, 2]) # 8
print("c[1, 3] = ", c[1, 3]) # 9
print("c[1, 4] = ", c[1, 4]) # 10
print("c[2, 0] = ", c[2, 0]) # 11
print("c[2, 1] = ", c[2, 1]) # 12
print("c[2, 2] = ", c[2, 2]) # 13
print("c[2, 3] = ", c[2, 3]) # 14
print("c[2, 4] = ", c[2, 4]) # 15
첫번째 인덱싱에는 행, 두번째 인덱싱에는 열이 들어갑니다. 이렇게 보니 보기 어려우니 아래처럼 출력할 수 있습니다.
for i in range(c.shape[0]) :
for j in range(c.shape[1]) :
print("c[%d, %d] = %d" % (i, j, c[i, j]), end=' ')
print()
# c[0, 0] = 1 c[0, 1] = 2 c[0, 2] = 3 c[0, 3] = 4 c[0, 4] = 5
# c[1, 0] = 6 c[1, 1] = 7 c[1, 2] = 8 c[1, 3] = 9 c[1, 4] = 10
# c[2, 0] = 11 c[2, 1] = 12 c[2, 2] = 13 c[2, 3] = 14 c[2, 4] = 15
그렇다면 3차원 배열에 대해서는 어떤 식으로 인덱싱을 하게 될까요? 2차원 배열을 여러개 쌓은 것이라고 보면 될 거 같습니다. 그러므로 인덱싱 순서는 행, 열, 깊이가 될 거 같습니다. 예시를 통해서 보도록 하겠습니다.
d = np.arange(1, 28).reshape((3, 3, 3))
print(d.shape) # (3, 3, 3)
print(d)
# [[[ 1 2 3]
# [ 4 5 6]
# [ 7 8 9]]
# [[10 11 12]
# [13 14 15]
# [16 17 18]]
# [[19 20 21]
# [22 23 24]
# [25 26 27]]]
print("d[0, 0, 0] = ", d[0, 0, 0]) # 1
print("d[1, 1, 0] = ", d[1, 1, 0]) # 13
print("d[2, 2, 0] = ", d[2, 2, 0]) # 25
print("d[0, 2, 2] = ", d[0, 2, 2]) # 9
print("d[1, 0, 1] = ", d[1, 0, 1]) # 11
2.2 다차원 배열의 특성
다차원 배열의 가장 좋은 특성 중 하나는 임의의 인덱스 정수 배열을 만들면 해당 인덱스 정수 배열에 따른 값을 추출해낼 수 있다는 점입니다. 예를 들어서 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 배열이 있다고 가정했을 때 1, 3, 7, 9와 같은 불규칙해보이는 배열을 인덱싱하고 싶다고 하면 어떤 식으로 진행하면 될 까요? 만약 리스트라면 슬라이싱을 할 수 없기 때문에 각 인덱스 번호를 찾아야합니다. 따라서 0, 2, 6, 8번 인덱스를 따로따로 인덱싱해야겠죠. 하지만 넘파이 객체는 이와 달리 매우 편한 인덱싱 기법을 지원합니다. 아래의 코드를 보면 이해가 빠를 것입니다.
e = np.arange(1, 11)
idx = np.array([0, 2, 6, 8])
print(e) # [ 1 2 3 4 5 6 7 8 9 10]
print(e[idx]) # [1 3 7 9]
이와 같이 인덱스 배열 idx를 넘파이 객체로 생성한 뒤 넘파이 배열 e의 인덱싱을 하게 되면 idx에 해당하는 값만 추출되게 됩니다.
인덱싱과 슬라이싱은 말로 설명하기 많은 한계가 있습니다. 그렇기 때문에 여러분들이 실제로 실습을 진행해보면서 인덱스 추출 연습을 해보시길 권장해드립니다.
'Programming > Python' 카테고리의 다른 글
넘파이 알고 쓰자 - Random Module (0) | 2020.08.22 |
---|---|
넘파이 알고 쓰자 - 브로드캐스팅 (0) | 2020.08.19 |
넘파이 알고 쓰자 - 넘파이 기본 연산 (3) | 2020.08.16 |
넘파이 알고 쓰자 - 넘파이 객체 선언 (0) | 2020.08.14 |
넘파이 알고 쓰자 - 소개 (0) | 2020.08.12 |