courses
이터레이터는 반복(iteration)할 수 있는 객체입니다. Python 언어의 공통 기능으로, 반복문과 리스트 컴프리헨션에서 자연스럽게 사용됩니다. 이터레이터를 뽑아낼 수 있는 모든 객체를 이터러블(iterable)이라고 합니다.
이터레이터를 구성하려면 많은 작업이 필요합니다. 예를 들어, 각 이터레이터 객체의 구현에는 __iter__()와 __next__() 메서드가 있어야 합니다. 위의 전제 조건 외에도, 객체의 내부 상태를 추적하는 방법이 있어야 하며 더 이상 반환할 값이 없을 때 StopIteration 예외를 발생시켜야 합니다. 이러한 규칙을 이터레이터 프로토콜이라고 합니다.
직접 이터레이터를 구현하는 일은 번거롭고, 항상 필요한 것도 아닙니다. 더 간단한 대안은 제너레이터 객체를 사용하는 것입니다. 제너레이터는 yield 키워드를 사용하여 한 번에 하나의 값을 순회할 수 있는 이터레이터를 반환하는 특별한 종류의 함수입니다.
어떤 상황에서 이터레이터를 구현하고, 어떤 상황에서 제너레이터를 사용할지 구분할 수 있는 능력은 Python 프로그래머로서의 역량을 높여 줍니다. 이 튜토리얼의 나머지 부분에서는 두 객체의 차이점을 강조하여 다양한 상황에서 어떤 것을 선택할지 결정하는 데 도움을 드리겠습니다.
용어집
|
용어 |
정의 |
|
이터러블 |
반복문으로 순회할 수 있는 Python 객체. 리스트, 집합, 튜플, 딕셔너리, 문자열 등이 이터러블에 해당합니다. |
|
이터레이터 |
반복할 수 있는 객체입니다. 따라서 이터레이터는 셀 수 있는 개수의 값을 포함합니다. |
|
제너레이터 |
단일 값을 반환하지 않는 특별한 함수 유형으로, 값의 시퀀스를 갖는 이터레이터 객체를 반환합니다. |
|
지연 평가(Lazy Evaluation) |
필요할 때에만 특정 객체를 생성하는 평가 전략입니다. 따라서 일부 개발자 커뮤니티에서는 지연 평가를 “call-by-need”라고 부르기도 합니다. |
|
이터레이터 프로토콜 |
Python에서 이터레이터를 정의할 때 따라야 하는 규칙 집합입니다. |
|
next() |
이터레이터에서 다음 항목을 반환하는 데 사용하는 내장 함수입니다. |
|
iter() |
이터러블을 이터레이터로 변환하는 데 사용하는 내장 함수입니다. |
|
yield() |
|
Python 이터레이터 & 이터러블
이터러블은 구성원을 하나씩 반환할 수 있는 객체로, 곧 순회가 가능합니다. 리스트, 튜플, 세트 같은 인기 있는 Python 내장 자료 구조는 이터러블에 해당합니다. 문자열과 딕셔너리 같은 다른 자료 구조도 이터러블로 간주됩니다. 문자열은 각 문자를 순회할 수 있고, 딕셔너리는 키를 순회할 수 있습니다. 일반적으로 for 반복문으로 순회할 수 있는 객체는 모두 이터러블로 보면 됩니다.
예제로 살펴보는 Python 이터러블
정의에 따르면, 모든 이터레이터는 이터러블이기도 합니다. 그러나 모든 이터러블이 반드시 이터레이터인 것은 아닙니다. 이터러블은 실제로 순회될 때에만 이터레이터를 만들어 냅니다.
이 기능을 보여 주기 위해 이터러블인 리스트를 인스턴스화하고, 리스트에 내장 함수 iter()를 호출하여 이터레이터를 만들어 보겠습니다.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
리스트 자체는 이터레이터가 아니지만, iter() 함수를 호출하면 이터레이터로 변환되어 이터레이터 객체가 반환됩니다.
모든 이터러블이 이터레이터가 아님을 보여 주기 위해, 동일한 리스트 객체를 만들고 이터레이터에서 다음 항목을 반환할 때 사용하는 next() 함수를 호출해 보겠습니다.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
위 코드에서 보듯이, 리스트에서 next() 함수를 호출하려 하면 TypeError가 발생합니다. 더 알아보기: Python의 예외 및 오류 처리. 이는 리스트 객체가 이터레이터가 아니라 이터러블이기 때문입니다.
예제로 살펴보는 Python 이터레이터
따라서 리스트를 순회하려면 먼저 이터레이터 객체를 생성해야 합니다. 그래야 리스트의 값들을 순서대로 제어하며 순회할 수 있습니다.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python은 이터러블 객체를 반복(loop)하려 할 때 자동으로 이터레이터 객체를 생성합니다.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
StopIteration 예외가 포착되면 루프가 종료됩니다.
이터레이터에서 얻는 값은 왼쪽에서 오른쪽으로만 가져올 수 있습니다. Python에는 이터레이터를 거꾸로 이동하게 해 주는 previous() 함수가 없습니다.
이터레이터의 지연 특성
같은 이터러블 객체를 기반으로 여러 개의 이터레이터를 정의할 수 있습니다. 각 이터레이터는 진행 상태를 자체적으로 유지합니다. 따라서 하나의 이터러블에 대해 여러 이터레이터 인스턴스를 만들면, 한 인스턴스는 끝까지 진행한 동안 다른 인스턴스는 시작 지점에 머물러 있을 수 있습니다.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
iterator_b가 시리즈의 첫 번째 요소를 출력하는 점에 주목하세요.
따라서 이터레이터는 지연 특성을 가진다고 할 수 있습니다. 이터레이터가 생성될 때 요소들은 요청되기 전까지는 생성되지 않습니다. 다시 말해, 우리의 리스트 인스턴스의 요소들은 next(iter(list_instance))로 명시적으로 요청할 때에만 반환됩니다.
하지만 이터레이터 객체에 내장 이터러블 컨테이너(예: list(), set(), tuple())를 호출해 이터레이터가 가진 모든 요소를 한꺼번에 생성하도록 강제하면, 이터레이터의 모든 값을 한 번에 추출할 수도 있습니다.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
이는 큰 이터레이터에는 권장되지 않습니다. 모든 요소를 한 번에 생성하여 메모리에 보관하게 되면, 지연 평가의 장점을 잃기 때문입니다.
데이터셋이 메모리에 편하게 담기에는 너무 크거나, 전체 이터레이터 클래스를 작성하지 않고도 지연 순회를 원할 때는 보통 제너레이터가 더 적합합니다.
Python 제너레이터
이터레이터를 직접 구현하는 가장 신속한 대안은 제너레이터를 사용하는 것입니다. 제너레이터는 겉보기에는 일반 Python 함수처럼 보이지만 다릅니다. 우선, 제너레이터 객체는 항목을 한 번에 반환하지 않습니다. 대신 yield 키워드를 사용해 필요한 순간에 항목을 생성합니다. 즉, 제너레이터는 지연 평가를 활용하는 특별한 종류의 함수라고 할 수 있습니다.
제너레이터는 일반적인 이터러블과 달리 내용을 메모리에 저장하지 않습니다. 예를 들어, 어떤 양의 정수의 모든 약수를 찾는 것이 목표라면, 보통은 전통적인 함수(이 튜토리얼에서 Python 함수에 대해 더 알아보세요)를 다음과 같이 구현할 것입니다.
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
위 코드는 약수 전체 리스트를 반환합니다. 하지만 전통적인 Python 함수 대신 제너레이터를 사용하면 차이가 있습니다:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
return 대신 yield를 사용했기 때문에, 함수는 실행 후 종료되지 않습니다. 본질적으로 Python에 전통적인 함수 대신 제너레이터 객체를 만들라고 지시한 것이며, 이를 통해 제너레이터 객체의 상태를 추적할 수 있게 됩니다.
결과적으로, 지연 이터레이터에서 next() 함수를 호출해 시리즈의 요소를 하나씩 확인할 수 있습니다.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
제너레이터를 만드는 또 다른 방법은 제너레이터 컴프리헨션을 사용하는 것입니다. 제너레이터 식은 리스트 컴프리헨션과 유사한 문법을 사용하지만, 대괄호 대신 소괄호를 사용합니다.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Python의 yield 키워드 살펴보기
yield 키워드는 제너레이터 함수의 흐름을 제어합니다. return을 사용할 때처럼 함수를 종료하는 대신, yield는 함수를 잠시 빠져나오면서 값을 반환하고, 로컬 변수의 상태를 기억합니다.
yield 호출에서 반환된 제너레이터는 변수에 할당할 수 있으며 next() 함수로 순회할 수 있습니다. 이는 함수가 처음 만나는 yield 지점까지 실행됨을 의미합니다. yield에 도달하면 함수 실행은 일시 중단되고, 그 상태가 저장됩니다. 따라서 원할 때 함수 실행을 다시 이어갈 수 있습니다.
함수는 마지막 yield 호출 이후부터 계속 실행됩니다. 예를 들어:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
위 코드에서 우리 제너레이터에는 yield 호출이 네 번 있지만, next를 다섯 번 호출하여 StopIteration 예외가 발생했습니다. 이는 제너레이터가 무한 시리즈가 아니므로 예상보다 더 많이 호출하면 고갈되기 때문입니다.
마무리
요약하면, 이터레이터는 순회 가능한 객체이며, 제너레이터는 지연 평가를 활용하는 특별한 함수입니다. 자체 이터레이터를 구현하려면 __iter__()와 __next__() 메서드를 작성해야 하지만, 제너레이터는 Python 함수나 컴프리헨션에서 yield 키워드를 사용해 구현할 수 있습니다.
복잡한 상태 유지 동작이 필요한 객체가 필요하거나 __next__(), __iter__(), __init__() 외의 메서드를 제공하고자 할 때는 사용자 정의 이터레이터가 제너레이터보다 나을 수 있습니다. 반면, 제너레이터는 내용을 메모리에 저장하지 않으므로 큰 데이터셋을 다룰 때나 굳이 이터레이터를 직접 구현할 필요가 없을 때 선호됩니다.
FAQS
Python에서 이터레이터와 제너레이터의 차이는 무엇인가요?
이터레이터는 __iter__()와 __next__()를 구현한 모든 객체입니다. 제너레이터는 yield 키워드를 사용하는 함수로 이터레이터를 더 간단히 만드는 방법입니다. 모든 제너레이터는 이터레이터이지만, 모든 이터레이터가 제너레이터인 것은 아닙니다.
Python에서 리스트 대신 제너레이터를 언제 사용해야 하나요?
대규모 또는 무한 시퀀스이거나 메모리 효율이 중요할 때는 제너레이터를 사용하세요. 리스트는 모든 요소를 한 번에 메모리에 보관하는 반면, 제너레이터는 한 번에 하나의 값만 생성합니다. 재사용할 소규모 데이터셋이라면 리스트로 충분한 경우가 많습니다.
Python에서 yield 키워드는 무엇을 하나요?
yield 키워드는 함수를 제너레이터로 바꿉니다. return처럼 종료하지 않고, yield는 함수를 일시 중지하고 값을 반환하며, 다음 호출에서 실행을 이어갈 수 있도록 상태를 기억합니다.
Python에서 제너레이터는 어떻게 만들죠?
함수에서 return 대신 yield를 사용하거나, 리스트 컴프리헨션과 동일한 문법에 괄호만 바꾼 제너레이터 식을 사용하세요. 예: (x * 2 for x in range(10)).
제너레이터가 Python에서 이터레이터보다 더 빠른가요?
원시 속도 자체가 더 빠르진 않지만, 필요한 순간에 값을 생성하므로 메모리 효율이 높습니다. 대규모 데이터셋에서는 전반적인 성능이 더 좋아지는 경우가 많고, 소규모에서는 차이가 미미합니다.