Python. slice 정리
1. 요약
Python에서 :는 단순한 기호가 아니라 슬라이스 표기법이다. 이 표기법은 내부적으로 slice 객체로 변환되어 리스트, 튜플, 문자열, NumPy 배열 등에서 범위 선택에 사용된다.
seq[start:stop:step]
의 의미는 다음과 같다.
start 이상부터 시작해서
stop 전까지 선택하고
step 간격으로 이동한다
중요한 점은 stop 위치의 원소는 포함되지 않는다는 것이다.
2. :는 표기법이고, slice는 객체다
코드에 보이는 :는 파이썬 문법의 일부다.
a[1:4]
이 표기는 내부적으로 대략 다음과 비슷하게 처리된다.
a.__getitem__(slice(1, 4, None))
즉 다음처럼 이해하면 정확하다.
코드에 보이는 : -> 슬라이스 표기법
내부적으로 만들어지는 것 -> slice 객체
직접 slice 객체를 만들어서 사용할 수도 있다.
a = [10, 20, 30, 40, 50]
s = slice(1, 4)
a[s]
# [20, 30, 40]
3. 기본 형태: start:stop:step
슬라이스의 전체 형태는 다음과 같다.
seq[start:stop:step]
예시:
a = [10, 20, 30, 40, 50, 60]
print(a[1:4]) # [20, 30, 40]
print(a[1:5:2]) # [20, 40]
각 부분의 의미는 다음과 같다.
| 표현 | 의미 |
|---|---|
start |
시작 인덱스 |
stop |
끝 인덱스. 단, 포함되지 않음 |
step |
이동 간격 |
a[1:4]
은 인덱스 1, 2, 3을 선택한다. 인덱스 4는 포함하지 않는다.
4. 생략 규칙
슬라이스에서는 start, stop, step을 생략할 수 있다.
a = [10, 20, 30, 40, 50]
| 표현 | 의미 | 결과 |
|---|---|---|
a[:] |
전체 선택 | [10, 20, 30, 40, 50] |
a[:3] |
처음부터 인덱스 3 전까지 | [10, 20, 30] |
a[2:] |
인덱스 2부터 끝까지 | [30, 40, 50] |
a[1:4] |
인덱스 1부터 4 전까지 | [20, 30, 40] |
a[::2] |
처음부터 끝까지 2칸씩 | [10, 30, 50] |
a[1::2] |
인덱스 1부터 끝까지 2칸씩 | [20, 40] |
생략된 값은 보통 다음처럼 해석된다.
start 생략 -> 처음부터
stop 생략 -> 끝까지
step 생략 -> 1칸씩
따라서 다음 둘은 같은 뜻이다.
a[:]
a[::]
둘 다 전체 범위를 선택한다.
5. 음수 인덱스와 슬라이스
Python에서는 음수 인덱스를 사용할 수 있다.
a = [10, 20, 30, 40, 50]
print(a[-1]) # 50
print(a[-2]) # 40
슬라이스에도 음수 인덱스를 쓸 수 있다.
print(a[-3:]) # [30, 40, 50]
print(a[:-1]) # [10, 20, 30, 40]
print(a[-4:-1]) # [20, 30, 40]
여기서도 stop은 포함되지 않는다.
a[-4:-1]
은 뒤에서 네 번째 원소부터 뒤에서 첫 번째 원소 전까지 선택한다.
6. 음수 step: 역방향 슬라이스
step에 음수를 쓰면 오른쪽에서 왼쪽으로 이동한다.
a = [10, 20, 30, 40, 50]
print(a[::-1])
# [50, 40, 30, 20, 10]
a[::-1]은 Python에서 매우 자주 쓰이는 관용구이며, 시퀀스를 뒤집는 의미다.
text = "hello"
print(text[::-1])
# 'olleh'
좀 더 복잡한 예:
a = [10, 20, 30, 40, 50, 60]
print(a[4:1:-1])
# [50, 40, 30]
해석:
인덱스 4에서 시작
인덱스 1 전까지 이동
step은 -1이므로 역방향
따라서 선택되는 인덱스는 4, 3, 2다. 인덱스 1은 포함되지 않는다.
7. 슬라이스는 원본을 복사하는가?
Python의 기본 리스트에서 슬라이스는 새 리스트를 만든다.
a = [10, 20, 30, 40]
b = a[:]
print(b)
# [10, 20, 30, 40]
print(a is b)
# False
a[:]는 전체 원소를 가진 새 리스트를 만든다. 그래서 리스트를 얕게 복사할 때 자주 쓰인다.
copy_a = a[:]
하지만 이것은 얕은 복사다. 내부에 중첩 리스트 같은 변경 가능한 객체가 있으면 내부 객체까지 깊게 복사하지는 않는다.
a = [[1, 2], [3, 4]]
b = a[:]
b[0][0] = 999
print(a)
# [[999, 2], [3, 4]]
바깥 리스트는 새로 만들어졌지만, 안쪽 리스트는 같은 객체를 공유한다.
8. 슬라이스에 대입하기
리스트에서는 슬라이스로 선택한 범위에 새 값을 대입할 수 있다.
a = [10, 20, 30, 40, 50]
a[1:4] = [200, 300]
print(a)
# [10, 200, 300, 50]
인덱스 1, 2, 3에 해당하던 [20, 30, 40]이 [200, 300]으로 바뀐다.
길이가 달라도 된다.
a = [10, 20, 30]
a[1:2] = [100, 200, 300]
print(a)
# [10, 100, 200, 300, 30]
빈 리스트를 대입하면 해당 구간을 삭제하는 효과가 있다.
a = [10, 20, 30, 40]
a[1:3] = []
print(a)
# [10, 40]
9. 슬라이스로 삭제하기
del과 함께 슬라이스를 사용할 수 있다.
a = [10, 20, 30, 40, 50]
del a[1:4]
print(a)
# [10, 50]
특정 간격의 원소를 삭제할 수도 있다.
a = [10, 20, 30, 40, 50, 60]
del a[::2]
print(a)
# [20, 40, 60]
10. 리스트, 튜플, 문자열에서의 slice
slice는 NumPy 전용이 아니다. Python 내장 시퀀스 타입에서도 사용된다.
리스트
a = [10, 20, 30, 40]
print(a[1:3])
# [20, 30]
튜플
t = (10, 20, 30, 40)
print(t[1:3])
# (20, 30)
문자열
text = "abcdef"
print(text[1:4])
# 'bcd'
리스트는 변경 가능하지만, 튜플과 문자열은 변경 불가능하다. 그래서 튜플이나 문자열에는 슬라이스 대입을 할 수 없다.
text = "abcdef"
# text[1:3] = "XX" # TypeError
11. slice() 함수로 직접 만들기
slice 객체는 직접 만들 수 있다.
s = slice(1, 4)
이것은 다음 표기와 같은 의미다.
1:4
단, 1:4는 인덱싱 문맥 안에서만 쓸 수 있는 표기법이다.
# s = 1:4 # SyntaxError
대신 이렇게 해야 한다.
s = slice(1, 4)
예:
a = [10, 20, 30, 40, 50]
s = slice(1, 4)
print(a[s])
# [20, 30, 40]
step까지 지정할 수도 있다.
s = slice(None, None, 2)
print(a[s])
# [10, 30, 50]
None은 생략을 뜻한다.
slice(None, None, None) -> [:]
slice(None, 3, None) -> [:3]
slice(1, None, None) -> [1:]
slice(None, None, 2) -> [::2]
slice(1, 5, 2) -> [1:5:2]
12. NumPy에서의 slice
NumPy에서도 같은 slice 객체를 사용한다. NumPy가 특별한 점은 여러 축(axis)에 대해 인덱싱을 동시에 할 수 있다는 것이다.
import numpy as np
A = np.array([
[10, 11, 12, 13],
[20, 21, 22, 23],
[30, 31, 32, 33],
[40, 41, 42, 43],
])
모든 행, 특정 열
A[:, 2]
# array([12, 22, 32, 42])
해석:
: -> 모든 행
2 -> 2번 열
즉 A[:, 2]는 “모든 행의 2번 열”이다.
특정 행, 모든 열
A[1, :]
# array([20, 21, 22, 23])
해석:
1 -> 1번 행
: -> 모든 열
즉 A[1, :]는 “1번 행의 모든 열”이다.
일부 행, 특정 열
A[:3, 2]
# array([12, 22, 32])
해석:
:3 -> 0번 행부터 3번 행 전까지
2 -> 2번 열
:와 ::의 관계
A[:, 1]
A[::, 1]
두 표현은 같다.
::는 start, stop, step을 모두 생략한 형태이므로 전체 범위를 1칸씩 선택한다.
A[::, 1]
은 내부적으로 다음과 비슷하다.
A.__getitem__((slice(None, None, None), 1))
13. Python 기본 리스트와 NumPy 배열의 차이
Python 기본 리스트도 slice 객체를 사용한다.
lst = [10, 20, 30, 40]
lst[1:3]
# [20, 30]
하지만 기본 리스트는 NumPy처럼 다차원 인덱싱을 직접 지원하지 않는다.
lst = [
[10, 11, 12],
[20, 21, 22],
[30, 31, 32],
]
# lst[:, 1] # TypeError
기본 리스트에서 특정 열을 가져오려면 보통 리스트 컴프리헨션을 쓴다.
[row[1] for row in lst]
# [11, 21, 31]
NumPy는 다차원 배열을 지원하므로 다음처럼 쓸 수 있다.
A[:, 1]
정리하면:
slice 객체 자체 -> Python 내장
리스트의 a[1:3] -> slice 객체 사용
문자열의 s[1:3] -> slice 객체 사용
NumPy의 A[:, 1] -> slice 객체 + 다차원 인덱싱 확장
14. 슬라이스와 차원 감소
NumPy에서 정수 인덱스를 쓰면 해당 축이 사라질 수 있다.
A[:, 2]
결과는 1차원 배열이다.
array([12, 22, 32, 42])
왜냐하면 열 방향에 2라는 정수 인덱스를 사용했기 때문이다.
반면 슬라이스를 쓰면 차원이 유지된다.
A[:, 2:3]
결과는 2차원 배열이다.
array([
[12],
[22],
[32],
[42]
])
비교:
A[:, 2] # 1차원 결과
A[:, 2:3] # 2차원 결과
이 차이는 머신러닝, 행렬 연산, 브로드캐스팅에서 중요하다.
15. 자주 쓰는 패턴 모음
전체 복사
b = a[:]
리스트의 얕은 복사.
처음 n개
a[:n]
마지막 n개
a[-n:]
n번째 이후 전부
a[n:]
마지막 원소 제외
a[:-1]
처음 원소 제외
a[1:]
뒤집기
a[::-1]
2칸씩 건너뛰기
a[::2]
역방향으로 2칸씩
a[::-2]
NumPy에서 모든 행의 특정 열
A[:, col]
NumPy에서 특정 행의 모든 열
A[row, :]
NumPy에서 일부 행과 일부 열
A[row_start:row_stop, col_start:col_stop]
16. 헷갈리기 쉬운 포인트
1. stop은 포함되지 않는다
a[1:4]
은 인덱스 1, 2, 3을 선택한다. 인덱스 4는 선택하지 않는다.
2. :만 쓰면 해당 축 전체다
a[:] # 1차원 전체
A[:, 1] # 모든 행의 1번 열
A[1, :] # 1번 행의 모든 열
3. :는 와일드카드가 아니라 슬라이스 표기다
A[:, 1]에서 :가 와일드카드처럼 보이지만, 정확히는 해당 축 전체를 선택하는 슬라이스다.
:
은 내부적으로 다음과 같다.
slice(None, None, None)
4. a[:]는 리스트에서는 새 리스트를 만든다
b = a[:]
b는 새 리스트다. 하지만 얕은 복사라는 점에 주의해야 한다.
5. NumPy 슬라이스는 view일 수 있다
Python 리스트 슬라이스는 새 리스트를 만들지만, NumPy 배열 슬라이스는 보통 원본 데이터를 공유하는 view를 만든다.
A = np.array([10, 20, 30, 40])
B = A[1:3]
B[0] = 999
print(A)
# array([10, 999, 30, 40])
NumPy에서는 이 점이 매우 중요하다. 원본과 독립된 복사본이 필요하면 .copy()를 사용한다.
B = A[1:3].copy()
17. 내부 동작 관점
다음 코드는:
a[1:4]
대략 다음과 비슷하다.
a.__getitem__(slice(1, 4, None))
다음 코드는:
a[:]
대략 다음과 비슷하다.
a.__getitem__(slice(None, None, None))
NumPy의 다음 코드는:
A[:, 2]
대략 다음과 비슷하다.
A.__getitem__((slice(None, None, None), 2))
즉 NumPy는 여러 축의 인덱싱 정보를 튜플로 받는다.
A[:, 2]
| |
| +-- 두 번째 축: 정수 인덱스 2
+----- 첫 번째 축: 전체 슬라이스
18. 기억할 핵심 문장
:는 슬라이스 표기법이다.- 슬라이스 표기법은 내부적으로
slice객체가 된다. seq[start:stop:step]에서stop은 포함되지 않는다.:만 쓰면 해당 축 전체를 뜻한다.::는start,stop,step을 모두 생략한 형태다.- 리스트, 튜플, 문자열, NumPy 배열 모두 Python의
slice개념을 사용한다. - NumPy는
slice를 다차원 배열의 각 축에 적용할 수 있게 확장해서 쓴다. - Python 리스트 슬라이스는 새 리스트를 만들지만, NumPy 슬라이스는 보통 view를 만든다.
- NumPy에서 정수 인덱스는 차원을 줄일 수 있고, 슬라이스는 차원을 유지한다.
A[:, 2]는 “모든 행의 2번 열”이고,A[:, 2:3]은 “모든 행의 2번 열을 2차원 형태로 유지”한다.
19. 최종 요약표
| 표현 | 내부적 의미 | 설명 |
|---|---|---|
a[:] |
slice(None, None, None) |
전체 선택 |
a[:3] |
slice(None, 3, None) |
처음부터 3 전까지 |
a[2:] |
slice(2, None, None) |
2부터 끝까지 |
a[1:4] |
slice(1, 4, None) |
1부터 4 전까지 |
a[::2] |
slice(None, None, 2) |
전체에서 2칸씩 |
a[::-1] |
slice(None, None, -1) |
역순 |
A[:, 1] |
(slice(None, None, None), 1) |
모든 행의 1번 열 |
A[1, :] |
(1, slice(None, None, None)) |
1번 행의 모든 열 |
A[:3, 2] |
(slice(None, 3, None), 2) |
처음 3개 행의 2번 열 |
A[:, 2:3] |
(slice(None, None, None), slice(2, 3, None)) |
모든 행의 2번 열을 2차원으로 유지 |