Search

파이썬 Iterable, Iterator, and Sequence

생성일
2023/04/03 11:56
태그
python
날짜
2023/04/04
3 more properties

개요

파이썬에서 순회(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
복사