이 글은 Effective Python을 참고하여 유용하다고 생각하는 방법을 선정하여 작성한 글입니다.

안녕하세요. jiogenes 입니다. 오늘은 시퀀스 슬라이스의 파이썬 다운 사용방법에 대해 알아보겠습니다.

파이썬은 시퀀스 타입(리스트, 튜플, range, 문자열)을 슬라이스 해서 조각으로 만드는 문법을 제공합니다. 시퀀스 슬라이스를 사용하면 시퀀스의 부분집합에 쉽게 접근할 수 있습니다. 내장된 시퀀스 타입이 아니더라도 __getitem____setitem__이라는 매직 메소드를 오버라이딩 한다면 커스텀 객체도 슬라이싱을 할 수 있습니다.

기본 문법

기본적인 문법은 sequence[start:end]이며 start는 포함, end는 포함하지 않습니다.

1
2
3
4
5
6
7
>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[:4]
[0, 1, 2, 3]
>>> a[-4:]
[5, 6, 7, 8]
>>> a[3:-3]
[3, 4, 5]

시퀀스의 처음 혹은 끝부분은 생략할 수 있습니다

1
2
assert a[:5] == a[0:5]
assert a[5:] == a[5:len(a)]

파이썬에서 시퀀스 슬라이싱은 매우 많이 사용되며 직관적으로 이해하기 쉬우므로 양 끝단 인덱스가 없는 형태로 사용하는것이 파이썬스러운 방법이라고 할 수 있습니다.

1
2
3
4
5
6
7
a[:]        # [0, 1, 2, 3, 4, 5, 6, 7, 8]
a[:5]       # [0, 1, 2, 3, 4]
a[:-1]      # [0, 1, 2, 3, 4, 5, 6, 7]
a[4:]       #             [4, 5, 6, 7, 8]
a[-3:]      #                   [6, 7, 8]
a[2:-1]     #       [2, 3, 4, 5, 6, 7]
a[-3:-1]    #                   [6, 7]

또한 시퀀스 슬라이싱은 start와 end인덱스가 리스트 경계를 벗어나도 사용할 수 있습니다.

1
2
a[:20]      # [0, 1, 2, 3, 4, 5, 6, 7, 8]
a[-20:]     # [0, 1, 2, 3, 4, 5, 6, 7, 8]

하지만 직접 인덱스에 접근하는 것은 에러가 발생합니다.

1
2
>>> a[20]
IndexError: list index out of range

슬라이싱 복사

슬라이싱의 결과는 완전히 새로운 리스트입니다.

1
2
3
4
5
6
7
8
9
10
b = a[4:]
print(f'before : {b}')
b[1] = 99
print(f'after : {b}')
print(f'origin : {a}')

>>>
before : [5, 6, 7, 8]
after : [5, 99, 7, 8]
origin : [1, 2, 3, 4, 5, 6, 7, 8]

따라서 다음과 같이 참조할때와 새로운 리스트를 복사할때의 코드는 다릅니다.

1
2
3
4
b = a       # 참조
assert a is b
b = a[:]    # 새로운 리스트 복사
assert a == b and a is not b

그러나 기본적으로 슬라이싱으로 하는 복사는 얕은 복사이기 때문에 시퀀스 내부에 또다른 시퀀스가 있을 경우 참조가 되어 원본을 수정할 수 있으니 주의해야 합니다.

1
2
3
4
5
6
7
8
9
10
a = [1, 2, 3, 4, 5, 6, 7, [1, 2, 3]]
b = a[:]
print(f'before : {b}')
b[-1][0] = 99
print(f'after : {b}')
print(f'origin : {a}')
>>>
before : [1, 2, 3, 4, 5, 6, 7, [1, 2, 3]]
after : [1, 2, 3, 4, 5, 6, 7, [99, 2, 3]]
origin : [1, 2, 3, 4, 5, 6, 7, [99, 2, 3]]

슬라이싱 할당

슬라이스를 할당에 사용하면 슬라이싱된 부분이 원본 시퀀스에서 지정한 범위를 대체합니다.

1
2
3
4
5
6
print(f'before : {a}')
a[2:7] = [100, 101, 102]
print(f'after : {a}')
>>>
before : [1, 2, 3, 4, 5, 6, 7, 8]
after : [1, 2, 100, 101, 102, 8]

시작과 끝 인덱스를 지정하지 않고 할당하면 원본 시퀀스 전체가 바뀝니다.

1
2
3
4
5
6
print(f'before : {a}')
a[:] = [100, 101, 102]
print(f'after : {a}')
>>>
before : [1, 2, 3, 4, 5, 6, 7, 8]
after : [100, 101, 102]

스트라이드

파이썬 슬라이싱은 다음과 같이 간격을 조절하는 스트라이드를 설정할 수 있습니다. sequence[start : end : stride] 이 문법을 사용하면 n번째 간격으로 시퀀스에서 슬라이싱을 할 수 있습니다. 직관적인 예시로 홀수 짝수 인덱스를 손쉽게 가져올 수 있습니다.

1
2
3
4
5
6
a = [1, 2, 3, 4, 5, 6, 7, 8]
print(a[::2])
print(a[1::2])
>>>
[1, 3, 5, 7]
[2, 4, 6, 8]

스트라이드는 음수값으로도 설정할 수 있습니다.

1
2
a[::2]      # [1, 3, 5, 7]
a[::-2]     # [8, 6, 4, 2]

스트라이드를 활용하면 이러한 문법을 사용할 수는 있습니다.

1
2
3
4
a[2::2]     # [3, 5, 7]
a[-2::-2]   # [7, 5, 3, 1]
a[-2:2:-2]  # [7, 5]
a[2:2:-2]   # []

하지만 이런 문법을 허용한다고 해서 막 사용하기에는 직관적으로 이해가 빨리 되지 않습니다. 대괄호 안에 숫자가 3개나 있으면 콜론(:)과 구분하기도 어렵고 어떤 의도로 작성됐는지 한번에 파악하기 어렵습니다. 특히 stride에 음수값이 있으면 더더욱 혼란스럽습니다.

따라서 파이썬스러운 슬라이싱 스트라이드 방법은 start, end, stride 인덱스를 한번에 사용하지 않는것입니다. 모두 사용해야 하는 경우에는 start, end인덱스로 슬라이싱하는 라인과 스트라이드를 하는 라인을 구분하여 따로따로 작성하는 것이 더 좋습니다. 스트라이드는 2이상의 수를 사용하므로 최소 절반 이상 시퀀스가 감소하기 때문에 스트라이드를 먼저 사용하는 것이 메모리 관리 측면에서 더 효율적입니다.

정리

  • 파이썬스럽게 슬라이싱 하자. 즉, start 인덱스에 0을 넣거나 end 인덱스에 len(sequence)를 넣지 말자.
  • 슬라이싱은 범위를 벗어난 인덱싱도 허용한다.
  • 슬라이싱 할당은 길이가 달라도 동작한다.
  • 한 슬라이싱에서 start, end, stride 인덱스를 한꺼번에 사용하면 매우 혼란스러울 수 있다.
  • stride에 음수값은 피하자.
  • start, end, stride 인덱스를 모두 사용해야 한다면 라인을 나눠서 사용하자.