개요
파이썬에서 순회(Iteration)은 리스트나 튜플등의 객체의 원소를 하나씩 가져와 for문 등을 통해 반복하는 중요한 기능이다. 이번 포스팅을 통해 Iterable객체, Iterator객체, Sequence 객체에 대해 살펴보자.
Iterable and Iterator Objects
파이썬에서 Iterable 객체란 __iter__ 메서드를 구현한 객체를 의미하며, Iterator 객체란 __next__ 메서드를 구현한 객체를 의미한다.
iterable → __iter__
iterator → __next__
루프문을 통해 iterable 객체를 순회할 때 __iter__ 메서드가 호출되고 iterator 객체가 반환된다. 그리고 루프 내부에서 해당 iterator 객체의 __next__ 메서드가 StopIteration Exception이 발생할 때까지 순회를 반복하게된다.
Python Iteration Protocol
파이썬에서 for문이 불리면 high level에서 다음과 같은 일이 발생한다. 1번이 실패하면 2번을 확인하는 식으로 Fall-back mechanism이 작동한다.
1.
객체에 __next__나 __iter__ 메서드가 구현되어있는지 확인한다. (iterable)
2.
__len__과 __getitem__ 이 구현되어 있는 지 확인한다. (sequence)
3.
TypeError를 발생시킨다
Playground
파이썬의 내장함수 iter(obj)는 obj.__iter__ 메서드를 호출하며, next(obj)는 obj.__next__를 호출하여 책임을 전가한다. 이를 토대로 간단한 실험 코드를 살펴보자.
l = [1, 2, 3]
print(l) # [1, 2, 3]
print(iter(l)) # <list_iterator object at 0x7fe3e167f880>
# print(next(l)) # TypeError: 'list' object is not an iterator
print(next(iter(l))) # 1
Python
복사
Sample Code: DateRangeIterable
Iterable 객체를 만들기 위해서는 __iter__ 메서드를 구현해야한다. 아래와 같이 날짜를 순회하는 클래스를 만들 수 있다.
from datetime import timedelta
class DateRangeIterable:
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._present_day = start_date
def __iter__(self):
return self # return an Iterator to iterate over
def __next__(self):
if (self._present_day >= self.end_date):
raise StopIteration()
today = self._present_day
self._present_day += timedelta(days=1)
return today
Python
복사
이 클래스는 iter과 next를 모두 구현하고 있으므로 iterable 객체이자 iterator 객체이다.
from datetime import date
for day in DateRangeIterable(date(2023, 3, 27), date(2023, 4, 2)):
print(day)
Python
복사
위의 코드를 실행하면 다음과 같은 일이 발생한다.
1.
DateRangeIterable 객체가 생성된다.
2.
for문 안에서 객체의 __iter__ 메서드가 호출되고, self가 반환된다.
3.
__next__ 메서드가 순차적으로 실행되며 return되는 값이 day 변수에 할당된다.
4.
StopIteration Exception이 발생하기 전까지 반복된다.
실행 결과는 다음과 같다.
2023-03-27
2023-03-28
2023-03-29
2023-03-30
2023-03-31
2023-04-01
Python
복사
객체의 재사용
위의 경우 __next__ 메서드 내부를 살펴보면 self_present_day가 end_date에 도달하면 재사용할 수가 없게 설계되어 있다. 따라서 다음과 같은 코드는 에러를 발생시킨다.
r1 = DateRangeIterable(date(2023, 1, 1), date(2023, 1, 5))
print(" - ".join(map(str, r1)))
# print(max(r1)) # ValueError: max() arg is an empty sequence
Python
복사
Container Iterable
위의 문제를 해결하기 위해 Container Iterable을 사용할 수 있다. __iter__ 메서드가 호출되면 self를 반환하는 대신 새로운 iterator 객체를 생성해서 반환한다.
class DateRangeContainerIterable:
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
def __iter__(self):
current_day = self.start_date
while current_day < self.end_date:
yield current_day
current_day += timedelta(days=1)
Python
복사
위의 예시에서는 제너레이터를 사용했다. 함수에 yield문이 있으면 해당 함수는 제너레이터가 되며 이로써 객체의 재사용이 가능하다. 제너레이터에 대해서는 추후 자세히 설명할 기회가 있을 것 같다.
이제 다음 코드는 문제 없이 작동한다.
r1 = DateRangeContainerIterable(date(2023, 1, 1), date(2023, 1, 5))
print(" - ".join(map(str, r1)))
print(max(r1)) # No more ValueError
Python
복사
Sequence
Sequence란 [] 연산(indexing operator)을 통해 원소에 접근할 수 있는 객체로 __getitem__과 __len__ 메서드를 구현해서 만들 수 있다. sequence를 만들면 특정 인덱스의 원소에 빠르게 접근 가능하지만 모든 값을 메모리에 올려두어야 하기 때문에 Memory - Time Trade Off가 발생한다. 다음은 위의 예시를 Sequence로 구현한 것이다.
class DateRangeSequence:
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._range = self._create_range()
def _create_range(self):
days = []
current_day = self.start_date
while current_day < self.end_date:
days.append(current_day)
current_day += timedelta(days=1)
return days
def __getitem__(self, day_no):
return self._range[day_no]
def __len__(self):
return len(self._range)
Python
복사
Sequence는 아래와 같이 indexing과 순회를 할 수 있다.
s1 = DateRangeSequence(date(2022, 1, 1), date(2022, 1, 5))
for day in s1:
print(day)
print('s1[0] -> ', s1[0])
print('s1[2] -> ', s1[2])
Python
복사
실행한 결과는 다음과 같다.
2022-01-01
2022-01-02
2022-01-03
2022-01-04
s1[0] -> 2022-01-01
s1[2] -> 2022-01-03
Python
복사