이전에 이터레이터(Iterator)와 제너레이터(Generator)에 대해 정리한 적이 있습니다. 이터레이터는 '반복 가능한 객체'를 순회하는 도구로, 이러한 객체의 요소를 하나씩 접근할 수 있게 해줍니다. 제너레이터는 이터레이터를 더 쉽게 생성할 수 있는 방법을 제공하며, yield 키워드를 사용하여 함수의 실행을 일시 중지하고 값을 반환할 수 있어, 대용량 데이터를 효율적으로 처리할 수 있게 해줍니다. yield에 대해서도 정리한 포스트가 있으니 참고하시기 바랍니다.
이전 포스트들로 충분히 이터레이터와 제너레이터에 대해 이해한 줄 알았지만 여전히 헷갈리는 부분이 있어서 이번엔 이터러블(Iterable)과 이터레이터를 비교/정리하는 포스트를 작성하도록 하겠습니다.
이터러블은 말 그대로 반복할 수 있는 객체를 의미합니다. 파이썬에서는 __iter__
메소드를 구현한 객체를 이터러블이라고 합니다. 이터러블 객체는 iter()
함수를 호출하여 이터레이터를 반환할 수 있습니다.
대표적인 이터러블에는 리스트, 튜플, 문자열 등이 있습니다. 이러한 객체들은 각각의 원소에 순차적으로 접근할 수 있기 때문에 이터러블로 분류됩니다.
예를 들어, 리스트 [1, 2, 3]
은 이터러블입니다. 이 리스트에 대해 iter()
함수를 호출하면 이터레이터가 반환됩니다.
이터러블은 자체적으로 next()
함수를 지원하지 않지만, 이터레이터를 통해 각 원소에 접근할 수 있게 됩니다. 앞서 말한대로 이터러블은 iter()
함수를 통해 이터레이터를 생성할 수 있습니다.
이터레이터는 값을 차례대로 꺼낼 수 있는 객체입니다. 파이썬에서는 __next__
메소드와 __iter__
메소드를 모두 구현한 객체를 이터레이터라고 합니다. next()
함수를 사용하여 이터레이터의 다음 값을 가져올 수 있습니다. 모든 값이 소진되면, 이터레이터는 StopIteration
예외를 발생시킵니다.
이터레이터의 주요 특징 중 하나는 현재 위치나 상태를 기억한다는 것입니다. 즉, 한 번 next()
함수가 호출되면 해당 위치의 값을 반환하고, 그 다음 위치로 이동하여 그 상태를 유지합니다. 따라서 다시 next()
함수를 호출하면 바로 그 다음 값이 반환됩니다.
간단한 예를 들면, [1, 2, 3]
리스트에 대한 이터레이터는 처음에는 1을 반환합니다. 다시 next()
를 호출하면 2를 반환하고, 한 번 더 호출하면 3을 반환합니다. 그 다음 호출에서는 StopIteration
예외가 발생합니다, 모든 원소를 이미 반환했기 때문입니다.
이터러블과 이터레이터는 깊게 연관된 두 개념입니다. 그 관계를 간단하게 정리하면 다음과 같습니다:
모든 이터레이터는 이터러블입니다. 즉, __iter__
메소드를 가진 이터레이터는 자기 자신을 반환합니다. 하지만 반대로, 모든 이터러블이 이터레이터는 아닙니다.
이터러블 객체는 iter()
함수를 통해 이터레이터를 얻을 수 있습니다. 이터러블의 __iter__
메소드는 이터레이터를 반환합니다. 이 반환된 이터레이터를 통해 next()
함수를 사용하여 원소에 순차적으로 접근할 수 있습니다.
이터레이터는 next()
를 통해 원소를 하나씩 반환하며, 다 반환하였을 때 StopIteration
예외를 발생시킵니다. 이터레이터는 상태를 유지하므로, 어디까지 원소를 반환했는지 알고 있습니다.
간단히 말하면, 이터러블은 반복 가능한 객체이며, 이터레이터는 그 반복을 실제로 수행하는 '작업자'와 같은 역할을 합니다. 반복문 (예: for
문)은 내부적으로 이터러블의 __iter__
메소드를 호출하여 이터레이터를 얻고, 이터레이터의 next()
메소드를 통해 원소를 하나씩 가져옵니다.
for
문에서 이터레이터가 사용되는 원리for
문이 어떻게 이터러블과 이터레이터와 상호 작용하는지, 그리고 StopIteration
예외 처리를 어떻게 하는지를 예제를 통해 정리해보겠습니다. 숫자를 차례대로 반환하는 간단한 이터러블/이터레이터 클래스를 만들어 보겠습니다.
class NumberSequence:
def __init__(self, start, end):
self._current = start
self._end = end
def __iter__(self):
return self
def __next__(self):
print('Called __next__')
if self._current >= self._end:
print('Raised StopIteration')
raise StopIteration('End of sequence')
value = self._current
self._current += 1
return value
sequence = NumberSequence(1, 4)
for number in sequence:
print(f"Number: {number}")
# 출력:
# Called __next__
# Number: 1
# Called __next__
# Number: 2
# Called __next__
# Number: 3
# Called __next__
# Raised StopIteration
이 예제에서 NumberSequence
클래스는 이터러블과 이터레이터의 역할을 동시에 합니다:
NumberSequence
객체를 생성할 때 시작 값과 끝 값을 지정합니다. 이 범위 내의 숫자들을 차례대로 반환합니다.
for
문은 NumberSequence
의 __iter__
메소드를 호출하여 이터레이터를 얻습니다. 여기서는 NumberSequence
객체 자체가 이터레이터입니다.
for
문은 이터레이터의 __next__
메소드를 호출하여 숫자를 하나씩 가져옵니다. 만약 현재 숫자가 끝 값에 도달하면, StopIteration
예외를 발생시킵니다. for
문은 이 예외를 내부적으로 처리하며 반복을 종료하지만, 예제에서는 이 부분을 확인하기 위해 Raised StopIteration
메세지를 출력하였습니다.
판다스의 DataFrame
에 관련해 많이 사용하는 .iterrows()
는 DataFrame의 각 행을 순회하는 메소드입니다. 이 메소드를 통해 반환되는 객체는 실제로 이터레이터입니다.
이터러블과 .iterrows():
DataFrame
은 이터러블입니다. 즉, __iter__
메소드를 구현하고 있습니다.df.iterrows()
는 이터레이터를 반환합니다. 이터레이터와 .iterrows():
.iterrows()
가 반환하는 객체는 __next__
메소드를 통해 다음 행의 데이터를 가져올 수 있습니다.import pandas as pd
df = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6]
})
iterator = df.iterrows()
print(next(iterator))
# 출력결과
(0, A 1
B 4
Name: 0, dtype: int64)
출력결과에 대한 설명:
위의 예제에서 df.iterrows()
를 호출하여 이터레이터를 얻고, next() 함수를 통해 인덱스와 행의 시리즈 데이터를 반환받을 수 있었습니다. 실사용시엔, for
문을 통해 .iterrows()
를 활용할 수 있습니다.