파이썬이 다른 언어보다 편리한 점 중 하나는 시퀀스 객체의 인덱싱(indexing)과 슬라이싱(slicing)이 편하다는 점이다.
Reference
1. 리스트의 마지막 요소 접근
파이썬은 리스트의 마지막 요소로를 -1이라는 인덱스로 접근이 가능하다.
다음과 같은 리스트 객체가 있다고 생각해보자.
nums = [1, 3, 5, 7, 9]
print(nums[-1]) # 9 출력
Python
복사
-k 라는 인덱스를 사용하면 뒤에서 k번째 원소에 접근이 가능한 것이다. 주의해야할 점은 이 역시 IndexError가 발생할 수 있다는 것이다. 파이썬의 인덱싱 범위를 다른 언어들과 비교해보면 다음과 같다.
nums = [1, 3, 5, 7, 9]
index = 0 1 2 3 4
=======================
Python 0 1 2 3 4
-5 -4 -3 -2 -1
=======================
Others 0 1 2 3 4
Plain Text
복사
nums 배열의 길이가 5인 경우는 다른 언어의 경우 (당연하게도) 0~4의 인덱스로 각각의 원소를 접근할 수 있다. 하지만 파이썬은 -1 ~ -5까지의 추가적인 범위로 접근 방법의 다양성을 제공한다. 하지만 파이썬 역시 위의 범위 이외의 인덱스로 접근하면 IndexError를 발생시킨다. 일반화 해서 이렇게 생각하면 좋을 듯하다.
길이가 N인 리스트에 대해서 보통은 0 에서 N-1 까지의 범위를 탐색하지만, 파이썬은 추가로 -1 ~ -N의 인덱싱을 추가로 제공한다.
2. 리스트 복사
파이썬은 인덱싱 뿐만 아니라 슬라이싱 기능도 지원한다.
nums = [1, 3, 5, 7, 9]
print(nums[1:4]) # [3, 5, 7]
Python
복사
위의 예시와 같이 nums[left:right]로 리스트를 슬라이싱하면 nums[left]와 nums[right-1]을 포함하는 리스트의 일부를 복사해서 반환한다. nums[right]은 포함되지 않는 다는 점에 주의하자.
만약 left와 right가 명시되지 않았다면 left는 0으로, right는 len(nums)로 처리된다.
깊은 복사와 얕은 복사
파이썬에서 리스트와 같은 Mutable 객체를 =를 통해 할당하면 주소값이 할당되는 얕은 복사가 진행된다. 하지만 리스트 슬라이싱은 슬라이싱된 복사본을 반환하므로 이를 이용하며 리스트의 복사본을 만들 수 있다. (슬라이싱하지 않는 슬라이싱을 이용한다)
nums1 = [1, 2, 3]
cp1 = nums1 # 얕은 복사
cp2 = nums2[:] # 깊은 복사 1
cp3 = nums[::] # 깊은 복사 2
Python
복사
다음과 같이 객제의 동일 유무를 할 수 있다.
print(id(nums1) == id(cp1)) # True
print(id(nums1) == id(cp2)) # False
print(id(nums1) == id(cp3)) # False
Python
복사
그럼에도 불구하고
객체를 복사할 때는 파이썬 내장 라이브러리인 copy의 deepcopy 함수를 사용하는 것을 추천한다.
다음 자가진단 Test를 푼다면 해당 해당 항목은 넘어가도 좋다.
Quiz
from copy import deepcopy
nums1 = [1, 2, 3]
deep_cp = deepcopy(nums1)
Python
복사
공식 문서의 내용은 단순하지만, 추천하는 이유는 온전한 deepcopy를 제공하기 때문이다. 우리는 단순히 1차원 리스트의 복사 같은 개념으로만 이해할 수 있지만, 2차원 이상의 배열을 사용한다면 주의할 필요하 있다. 길게 설명하지 말고, 코드로 이해해보자.
from copy import deepcopy
nums1 = [[1, 2], [3, 4]]
cp = nums[:]
Python
복사
우리는 여기서 cp에 nums의 깊은 복사본을 만들어 할당했다고 생각할 수 있다. 하지만 해당 코드가 한 일은 nums의 각 원소를 가져와서 새로운 배열에 담고, 그 배열을 cp에반환한 것이다.
그리고 여기서 nums의 각 원소 역시 리스트(mutable object)이므로 주소가 할당되어 실제로는 부분적으로만 깊은 복사가 된 것이다. recursive하게 true deep copy가 필요하다면 내장 라이브러리를 활용하자.
from copy import deepcopy
nums = [[1, 2], [3, 4]]
cp1 = deepcopy(nums)
cp2 = nums[:]
cp1[0][0] = -1
print(f'{nums[0][0]=}') # nums[0][0]=1
cp2[0][0] = -1
print(f'{nums[0][0]=}') # nums[0][0]=-1
Python
복사
3. [start:end:step]
사실 인덱싱은 부분수열만 가능한 것은 아니다. step을 주어 원소를 간격을 두며 접근할 수도 있다.
nums = [1, 3, 5, 7, 9]
print(nums[1:5:2]) # [3, 7]
Python
복사
위의 예시에서 nums[1:5:2]는 인덱스1 원소부터 시작하여 인덱스5 원소 전까지 2 step 간격으로 원소에 접근한다.
4. 슬라이싱을 위한 slice 객체 (slice는 namedtuple?)
파이썬에서는 slice라는 built-in function을 통해 slice 객체를 생성할 수 있다. 공식 문서를 살펴보면,
They have no other explicit functionality; however …
→ 특별한 기능이 없다
사실 별 기능이 없다. slice는 하나의 약속 같은 개념으로 namedtuple과 같은 형태로 제공된다고 이해할 수 있다. 파이썬이 인덱싱과 슬라이싱을 수행할 때는 start, stop, 그리고 step에 대한 정보만 필요하기 때문이다. 당연한 얘기지만, namedtuple 자체가 slice 기능을 지원한다는 것은 아니다.
# 사용하지 않은 변수는 None으로 지정
interval = slice(None, 3) # [:3] 과 같은 역할을 수행
print(nums[interval] == nums[:3]) # True
Python
복사
아래의 실험 코드를 살펴보자.
from collections import namedtuple
TupleSlice = namedtuple("TupleTypeSlice", ['start', 'end', 'step'])
tuple_slice = TupleSlice('aaa', 123, 'ddd') # namedtuple을 이용한 sudo slice
real_slice = slice('aaa', 123, 'ddd') # 실제 slice
print(real_slice) # slice('aaa', 123, 'ddd')
print(tuple_slice) # TupleTypeSlice(start='aaa', end=123, step='ddd')
print(real_slice.start, tuple_slice.start) # aaa aaa
Python
복사
물론 여기서 작성한 real_slice는 실제 슬라이싱에 활용될 수 없다. 다만, 실제로 엄격한 functionality가 구현되 지 않고, 개념적으로는 namedtuple과 같은 역할을 수행하고있다는 것을 확인할 수 있다.
5. [] 을 위한 __getitem__과 __len__ 매직 메서드
파이썬의 시퀀스 객체들은 인덱싱과 슬라이싱을 위해 내부적으로 __getitem__과 __len__을 이용한다. 직접 class에 매직 메서드를 구현해서 확인하면 이해가 쉬울 것이다. 다음 코드는 __getitem__이 호출되는 과정을 보여준다.
# 테스트용 시퀀크 클래스
class MySequence:
def __init__(self):
...
# [item]로 접근시 호출되는 매직 메서드
def __getitem__(self, item):
item_type = type(item)
print(f'{item} is {item_type} type')
if item_type == int: # 정수가 들어오면 인덱싱
print(f'implement sequence indexing here')
elif item_type == slice: # 슬라이스가 들어오면 슬라이싱
print(f'implement sequence slicing here')
else: # 그 이외의 타입은 일단 예외처리
raise ValueError('Unhandled type for either indexing and slicing')
def __len__(self):
...
seq = MySequence()
Python
복사
위에서 나만의 MySequence라는 클래스를 만들고 []의 접근을 정의하기 위한 __getitem__을 구현했다. 그리고 seq 객체를 생성했다. 이제 인덱싱과 슬라이싱을 테스트 해보자.
인덱싱 테스트
seq[1]
# seq[:3]
# seq['hi']
""" 실행 결과
1 is <class 'int'> type
implement sequence indexing here
"""
Python
복사
슬라이싱 테스트
# seq[1]
seq[:3]
# seq['hi']
""" 실행 결과
slice(None, 3, None) is <class 'slice'> type
implement sequence slicing here
"""
Python
복사
그 외의 타입 테스트
# seq[1]
# seq[:3]
seq['hi']
""" 실행 결과
hi is <class 'str'> type
ValueError: Unhandled type for either indexing and slicing
"""
Python
복사