_빌런 2023. 11. 11. 23:58

1. Iterator

2. Iterator Protocol

3. Constraints

4. Iterator vs Iterable

5. Collection vs Sequence

6. Collections 라이브러리

7. Itertools 라이브러리

8. Appendix


1. Iterator

Iterator라는 개념은 Python 2.2 버전에서 PEP 234에 의해 추가되었다.

반복하는 프로세스에 대한 통일된 추상화 규격이 필요했기 때문이다.

 └ 반복하는 프로세스: 하드 코딩 반복, indefinite iteration(무기한 반복, while), definite iteration(기한 반복, for)

추상화를 통해 collections과 unordered collections에 대해서 확실한 규격을 갖게 되었다.

 └ 여기서 collections는 변수나 아이템들이 모여있는 그룹(추상화 데이터 타입)을 말한다.

 

결국 파이썬에서 Iterator는 data collections을 순회하는 객체다.

파이썬에서 Iterator는 디자인 패턴 중 Iterator Pattern으로 구현한다.

또한 Iterator Protocol이라는 내부 구조를 구현해야 한다.

이를 통해 list, tuple, dictionary, set 같은 자료 구조의 요소(element)에 접근할 수 있다.

 

Iterator의 핵심 행동은 다음과 같다.

1. 한 번에 하나의 아이템을 반환하기

2. 현재 방문한 아이템 추적하기

 

2. Iterator Protocol

https://realpython.com/python-iterators-iterables/

Iterator Protocol은 위의 두 메소드를 말한다.

파이썬의 공식 iterator든, 직접 만든 custom iterator class든, iterator protocol이 들어가야만 한다.

그래야 Iterator라고 부를 수 있다.

 

__iter__() 메소드는 일반적으로 self를 반환하는데, 이는 현재 객체인 iterator 자신에 대한 참조를 가진다.

__iter__()의 유일한 책임은 iterator 객체(self, 현재 인스턴스)를 반환하는 것이다.

 

__next__() 메소드는 데이터 스트림에서 다음 아이템을 반환한다.

더 이상 스트림에 아이템이 없을 때는 흐름 제어를 위해 StopIteration 예외를 발생시킨다.

 

class SequenceIterator:
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < len(self._sequence):
            item = self._sequence[self._index]
            self._index += 1
            return item
        else:
            raise StopIteration
    
        
# Sample Code
si = SequenceIterator([1, 2, 3, 4])

print(si.__next__())    # 1
print(si.__next__())    # 2
print(si.__next__())    # 3
print(si.__next__())    # 4
print(si.__next__())    # StopIteration

해당 Iterator Protocol을 사용하여 직접 custom iterator class를 만들었다.

객체를 초기화할 떄, 특정 sequence 자료를 입력받고, index를 0으로 한다.

그리고 __next__()가 호출될 때마다, index에 1을 더하여 해당 index의 요소를 반환한다.

가장 처음 입력받는 인스턴스의 길이보다 index가 커지게 되면, StopIteration을 발생한다.

 

sequence = SequenceIterator([1, 2, 3, 4])

# Get an iterator over the data
iterator = sequence.__iter__()

while True:
    try:
        # Retrieve the next item
        item = iterator.__next__()
    except StopIteration:
        break
    else:
        # The loop's code block goes here...
        print(item)

객체를 생성하여 접근하는 코드는 위와 같다.

SequenceIterator로 [1, 2, 3, 4]를 넘겨준 뒤, sequence 인스턴스에 할당한다.

__iter__()로 SequenceIterator의 데이터를 가져온 뒤, 반복문으로 요소에 접근한다.

반복문에서는 try - except - else를 활용하여, except로 빠지지 않으면 else로 이동하여 코드를 실행한다.

iterator.__next__()로 다음 item을 받아온 뒤, 오류라면 break 아니라면 해당 아이템을 출력한다.

 

3. Constraints

# Sample Code
numbers_iter = SequenceIterator([1, 2, 3, 4])

# 1, 2, 3, 4를 한 줄마다 출력
for number in numbers_iter:
    print(number)
    
# 아무 것도 출력하지 않음
for number in numbers_iter:
    print(number)

위에서 작성한 SequenceIterator를 사용하여 두 번의 반복문을 돌려보자.

첫 번째 반복문에서는 numbers_iter에 넣어준 모든 요소를 정상적으로 출력하지만, 두 번째에서는 아니다.

이는 첫 번째 반복문과 두 번째 반복문이 이어지기 때문이다.

즉 첫 번째 반복문으로 해당 iterator는 모든 요소를 소진했다.

두 번째 반복문을 시작할 때 이미 전부 소진했기 때문에, StopIteration을 뱉고 종료한다.

 

# Sample Code
numbers_iter = SequenceIterator([1, 2, 3, 4])

# 1, 2, 3, 4를 한 줄마다 출력
for number in numbers_iter:
    print(number)
    
another_iter = SequenceIterator([1, 2, 3, 4])
    
# 1, 2, 3, 4를 한 줄마다 출력
for number in another_iter:
    print(number)

따라서 같은 요소에 대해서 여러 번 순회하고 싶다면, 똑같은 객체를 여러 번 재할당해야만 한다.

위의 코드는 numbers_iter에 [1, 2, 3, 4]를 할당, 순회한 뒤 다시금 할당, 순회를 하는 예제다.

 

numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

# 1, 2, 3을 한 줄마다 출력
for number in numbers_iter:
    if number == 4:
        break
    print(number)

# 5를 출력
next(numbers_iter)

# 6을 출력
next(numbers_iter)

# StopIteration
next(numbers_iter)

위에서 언급했듯이, Iterator는 현재 위치를 기억하고 아이템을 방문한다.

요소 개수가 명확하다면(위의 코드에서는 4), 조건에 도달하기까지 더 이상 아무 것도 하지 않아도 된다.

__next__() 메소드만 정의했다는 것은 바꾸어말하면, 이전으로는 돌아갈 수 없음을 뜻한다.

즉 __previous__() 메소드를 통하여 이전 index로 향할 수 없다.

 

만약 4에 도달했다고 해보자. 이때 numbers_iter는 앞으로 얼마나 많은 요소를 순회할 수 있을지 알까?

이전 상태로 되돌아갈 수 없다는 점 외에, 동시에 전체 길이를 파악할 수 없다는 점도 있다.

왜냐하면 Iterator는 한 번에 하나의 요소만을 저장하기 때문이다.

Iteraotr가 유한하다는 전제 하에, 전체 요소를 돌아야만 길이를 파악할 수 있다.

 

numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

numbers_iter[2]
'''
Traceback (most recent call last)
TypeError: 'SequenceIterator' object is not subscriptable
'''

numbers_iter[1:3]
'''
Traceback (most recent call last)
TypeError: 'SequenceIterator' object is not subscriptable
'''

또 다른 문제점으로는 접근 방법이다.

Iterator는 한 번에 하나의 요소에 순차적으로 접근한다는 특징을 가지고 있다.

그렇기에 특정 인덱스 위치로 접근하는 indexing이나, 범위를 추출하는 slicing이 불가능하다.

 

이처럼 Iterator 여섯 가지 제약 사항을 확인할 수 있다.

 특정 iterator에 대해서 한 번 이상 순회(반복)할 수 없다.

ㆍ iterator의 모든 요소를 소진했을 때, 재시작하도록 초기화할 수 없다.

ㆍ 오직 앞으로(forward)만 갈 수 있고, 되돌아(backward)갈 수 없다.

ㆍ 끝에 도달하기 전까지는 전체 길이를 알 수 없다.

ㆍ list, tuple처럼 대괄호[ ]를 사용한 indexing과 slicing이 불가능하다.

 

class ReusableRange:
    def __init__(self, start=0, stop=None, step=1):
        if stop is None:
            stop, start = start, 0
        self._range = range(start, stop, step)
        self._iter = iter(self._range)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self._iter)
        except StopIteration:
            self._iter = iter(self._range)
            raise

물론 첫 번째와 두 번째 상황은 실제 Iterator에 적용되는 문제점은 아니다.

해결 방법은 위와 같은 코드를 사용하여, StopIteration에 걸릴 시 본래 값으로 초기화해준다.

위 코드에 존재하는 next() 함수는 built-in function으로 __next__() 메소드를 호출하는 함수다.

반복문 없이 iterator를 순회할 수 있기에, 무한이나 길이를 모르는 iterator를 다룰 때 유용하게 사용할 수 있다.

또한 next() 함수는 두 번째 매개변수를 갖는데, 이는 StopIteration이 발생했을 때 반환할 값을 말한다.

 └ Ex) next([1, 2, 3], 0) next를 통해 1, 2, 3 요소를 전부 순회하고 다시 next()로 접근한다면(StopIteration) 0을 반환한다.

 

4. Iterator vs Iterable

가장 첫 1. Iterator 챕터에서 설명했듯 iter라는 것은 '반복'이라는 뜻이다.

Iterator는 '모여있는 아이템을 순회하는 객체'라고 설명했다.

하지만 세간에는 Iterator라는 용어도 있고, Iterable이라는 용어도 있다.

Iterator가 '순회하는 객체'라면, 직관적으로 Iterable은 '순회 가능한'이라는 뜻이다.

사각형과 직사각형의 관계처럼, Iterable이 Iterator의 상위 개념처럼 보인다.

엄밀히 따지자면, 두 개념은 종속하지 않는 다른 개념이다.

 

https://realpython.com/python-iterators-iterables/#comparing-iterators-vs-iterables

Iterator와 Iterable를 구분하는 차이는 __next__() 메소드의 유무다.

클래스를 구현하는 내부 구조에 __next__() 메소드가 있으면 Iterator, 없으면 Iterable이다.

 └ 추가로 Iterable에는 __iter__() 또는 __getitem__() 메소드가 있어야 한다(PEP 234).

 └ __iter__()와 __getitem__()을 Iterable Protocol이라고 부른다.

하지만 실무에서는 iterable 안에 iterator가 포함하게끔 설계한다고 한다.

직접 클래스를 설계할 때도 iterator가 iterable에 포함하도록 설계하는 원칙을 따라간다고 한다.

 

밑에서 부연 설명을 하겠지만, Iterator에는 파일 객체, generator, itertools 내의 모듈 등이 해당한다.

놀랍게도 Iterable에는 list, tuple 등이 해당한다.

Iterable에는 __next__()가 없어서 __iter__()로 객체를 호출하더라도 늘 같은 값이 나온다.

왜냐하면 __iter__() 메소드는 '객체 자신을 반환'하는 책임밖에 없기 때문이다.

이와 반대로 Iterator는 호출자의 요구가 있을 때만 한 요소씩 반환한다.

그렇기에 Iterator가 Iterable보다 메모리 측면에서 효율적이다.

 

list 내부 구조(좌상단), dict 내부 구조(좌하단), tuple 내부 구조(우상단), set 내부 구조(우하단(

이런 이유로 Python에서 사용하는 list, dict, tuple, set은 전부 Iterable 객체다.

위 사진에 첨부하지는 않았지만, str도 Iterable 객체에 해당한다(__iter__(), __getitem__() 메소드가 전부 존재한다).

3. Constraints 챕터 마지막 부분에서 next() 함수는 __next__() 메소드를 호출하는 함수라고 했다.

고로 위에서 언급한 Iterable 객체들은 next()를 호출하는 것이 불가능하다.

이런 Iterable 객체를 iter() 함수로 감싸 순회하는 것이 가능하다. 이것이 range() 함수 동작 원리다.

Iterator라면 next()로 요소들을 하나씩 확인하고, Iterable이라면 iter()를 씌운 뒤 next()로 요소들을 하나씩 확인한다.

 

5. Collection vs Sequence

Iterable하다는 것은 '해당 클래스 내부 구조에 __iter__()나 __getitem__()이 있는 순회 가능한 객체'임을 알았다.

Iterable한 객체 형태에 따라서 두 가지 타입으로 나눌 수 있다.

하나는 Collection 타입이고, 다른 하나는 Sequence 타입이다.

 

Collection 타입

개념 : 데이터가 서로 관련이 없는, 여러 개의 요소를 가질 뿐인 데이터

종류 : set, dictionary, default dictionary, OrderedDict, Counter 등

ㆍ 멤버쉽 연산자(in)를 사용할 수 있음 ㆍ 크기(길이)가 있음 ㆍ 순회 가능

 

Sequence 타입

개념 : 여러 개의 요소가 있으며, 요소에 순서가 존재하는 데이터 타입

종류 : list, tuple, str

ㆍ 멤버쉽 연산자(in)를 사용할 수 있음 ㆍ 크기(길이)가 있음 ㆍ 순회 가능

ㆍ 순서가 유지 ㆍ 정수를 통한 indexing과 slicing 가능 ㆍ 복제 가능(*N) ㆍ 같은 타입은 잇는 것이 가능(s + s)

 

Collection과 Sequence 타입에서 가장 큰 차이는, '순서가 존재하느냐'이다.

각 요소에 대해 하나씩 반환은 가능하지만, 정해진 순서가 없는 set과 dict은 collection이다.

각 요소에 대해 하나씩 반환이 가능하며, 순서대로 나열되어 있는 list와 tuple은 sequence이다. 

 

이때 Collection은 가장 처음에 언급한 Collections하고는 다르다.

Collections는 변수나 아이템들이 모여있는 그룹을 말한다. 하나의 거대한 집합 같은 개념이다.

따라서 list, stack, queue, deque, priority queue, set, array, graph, tree 등이 전부 해당한다.

파이썬으로 코딩테스트를 준비하거나 알고리즘을 공부한 사람이라면 collections가 뭔가 낯익을 거다.

알고 있는 그 collections가 맞다.

 

6. Collections 라이브러리

collections는 어떤 아이템이 모여있는 그룹, 위에서 설명한 다른 자료구조들을 모아둔 라이브러리다.

위의 사진은 dir() 함수로 collections에서 호출할 수 있는 목록을 출력한 것이다.

ChainMap, Counter, OrderedDict, defaultdict, deque, namedtuple 등

어떤 아이템들에 대해서 모아두는 자료구조들만을 정리한 라이브러리다.

파이썬에서 자주 사용하는 두 라이브러리(collections, itertools) 중 하나이다.

itertools에서 iter라는 단어가 보인다. iterable과 어떤 연관이 있을까?

 

7. Itertools 라이브러리

itertools는 iterator를 위한 다양한 도구를 제공하는 라이브러리다.

위의 사진은 dir() 함수로 itertools에서 호출할 수 있는 목록을 출력한 것이다.

count, cycle, repeat 등의 무한한 sequence를 생성하는 iterable 객체 목록,

accumulate, chain, groupby, takewhile, dropwhile 등의 종료 조건이 있는 iterable 객체 목록,

permutations, combinations, product 등의 데이터 조합을 생성하는 목록.

이것이 itertools 구성이다.

 

말그대로 collections는 데이터들의 집합을 모아둔 라이브러리고,

itertools는 순회 가능한 함수를 모아둔 라이브러리에 해당한다.

앞으로는 헷갈리지 않고 적재적소에 사용할 수 있다. 

 

8. Appendix

참고한 문서 자료 : https://realpython.com/python-iterators-iterables/

참고한 문서 자료 : https://docs.python.org/3/whatsnew/2.2.html#pep-234-iterators

참고한 문서 자료 : https://peps.python.org/pep-0234/

참고한 문서 자료 : https://en.wikipedia.org/wiki/Collection_(abstract_data_type)

참고한 문서 자료 : https://en.wikipedia.org/wiki/Iterator_pattern

참고한 문서 자료 : https://docs.python.org/3/glossary.html#term-sequence

참고한 문서 자료 : https://docs.python.org/3/library/functions.html#next

참고한 문서 자료 : https://wikidocs.net/84391

참고한 웹 사이트 : https://lgphone.tistory.com/38

참고한 웹 사이트 : https://www.acmicpc.net/board/view/102619

참고한 웹 사이트 : https://velog.io/@ehdgus8054/파이썬-컬렉션-자료구조

참고한 웹 사이트 : https://tibetsandfox.tistory.com/27

참고한 웹 사이트 : https://born-dev.tistory.com/18