티스토리 뷰
Reference
- Fluent Python
- docs.python.org
- Stackoverflow
- geeksforgeeks
for구문의 동작원리?
이터레이터와 이터러블에 대해 알기 전에
우리에게 너무나도 익숙한 코드 한 줄을 봐보겠다.
number = {'one': 1, 'two': 2, 'three': 3}
for i in number:
print(i)
이런 간단한 반복문 실행 결과는
파이썬을 조금이라도 배운 사람들에겐
누워서 떡먹기다.
예상대로
number 딕셔너리의 키값들이 콘솔에 출력된다.
그런데 호기심이 많은 필자에게 매번 for구문은 마법처럼 느껴졌다.
어떤 값이 출력되는지는 알지만
어떻게 동작하는지는 정확히 알지 못했다.
그냥 for구문 문법대로 쓰면
for구문이 알아서 해당 객체의 값을 반복시켜줬다.
for구문은 어떻게 동작하는 걸까?
Iterater, Iterable
for구문의 동작 과정을 이해하려면 이터레이터를 알아야 한다.
이터레이터라는게 뭘까?
답은 간단하다.
말 그대로 반복자라는 뜻이다.
파이썬에서 특정(x) 객체를 반복해야 할 때
언제나 파이썬 인터프리터는 자동적으로 iter(x)를 호출한다.
이 뜻은
반복을 실행해야 될 때
해당 객체의 반복자(iterater) 객체가 필요하다는 뜻이다.
반복 연산 외에도
- 컬렉션형 생성과 확장
- 텍스트 파일을 한 줄씩 반복
- List, Dict, Set Comprehensions
- 튜플 언패킹
- 함수 호출 시 *를 이용한 매개변수 언패킹
이런 연산을 하기 위해선
해당 객체의 반복자 객체가 필요하다.
위에서 필자는 특정 객체(x)가 반복자(iterater)를 생성하기 위해서
iter() 함수를 호출한다는 걸 통해서
당연히 반복을 하기 위한 특정 객체(x)는 iter() 함수를 호출할 수 있어야 하기 때문에
__iter__() 매직 메서드가 구현되어 있는 상태가 돼야 한다는 걸 유추할 수 있다.
하지만 한 가지 예외가 있다
파이썬 시퀀스 프로토콜인 __getitem__()을 구현해도
iter() 함수를 사용할 수 있다.
(★Fluent Python에 의하면 하위 버전과의 호환성 문제 때문이라고 한다★)
이렇게 iter() 함수가 반복자를 가져올 수 있는 모든 객체(__getitem__()을 구현한)와 반복자를 반환하는
__iter__() 매직 메서드를 구현한 객체를 '반복형'이라고 한다.
반복자를 생성할 수 있는
반복형이 되기 위해선
__iter__() or __getitem__() 매직 메서드가 구현되어있어야 한다.
(__iter__()는 이터러블(반복형)의 프로토콜이라는 걸 알 수 있다.)
class Sentence:
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f'Sentence({self.text})'
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self
위 코드를 보면
Sentence객체에 __iter__() 매직 메서드가 구현되어 있는 걸 확인할 수 있다.
그렇다면 우린 Sentence객체가 반복형이란 걸 바로 알 수 있다.
iter() 함수를 통해 반복자 객체를 생성해보자
x = Sentence('Hello World!')
y = iter(x)
print(y)
>>> <__main__.SentenceIterator object at 0x100cfce20>
성공적으로 반복자 객체가 생성되었지만
아직 이 반복자 객체가 무슨 일을 하는지 잘 모르겠다.
아까 필자가 위에서 언급했던
파이썬에서 특정(x) 객체를 반복해야 할 때
언제나 파이썬 인터프리터는 자동적으로 iter(x)를 호출한다.
이 문장을 잘 보면 반복자가 어떤 일을 하는지
추측할 수 있다.
답은 간단하다.
반복해야할 때 반복자를 만들었으면
그 반복자는 당연히
반복형의 값들을 순차적으로 반환할 수 있어야 하지 않겠는가?
반복자는 이러한 과정을
next() 메서드를 통해 해결한다.
(__next__()는 이터레이터(반복자)의 프로토콜이라는 걸 알 수 있다.)
반복자(iterater)는 다음 항목을 반환하거나, 다음 항목이 없을 때 StopIteration 예외를 발생시키고
인수를 받지 않는 __next__() 메서드를 구현하는 객체이다.
-Fluent Python(반복자)-
한 번 위에서 생성한 반복자(y)의 next()메서드를 호출해보겠다.
x = Sentence('Hello World!')
y = iter(x)
print(y.words)
>>> ['Hello', 'World!']
print(next(y))
>>> Hello
print(next(y))
>>> World!
print(next(y))
>>> StopIteration
y객체 SentenceIterator에서 정의한 __next__() 매직 메서드가
잘 동작함으로써 Sentence객체의 반복자 역할을 수행하는
모습을 확인할 수 있었다.
for구문의 동작원리!
이제 for 구문이 어떤 동작 과정을 거치는지
어느 정도 짐작이 가지 않는가?
number = {'one': 1, 'two': 2, 'three': 3}
for i in number:
print(i)
실제로 for구문이 실행될 때 이러한 과정을 거친다.
- 파이썬 인터프리터는 자동으로 iter(number)를 호출해서 반복자(iterater) 객체를 가져온다
- 만약 __iter__()가 구현되어 있지 않아서 iter()를 호출하지 못한다면 __getitem__()메서드가 구현되어 있는지 확인한다.
- 만약 __getitem__()메서드가 구현되어 있다면 인덱스 [0]부터 IndexError(Exeption처리)가 나올때가지 getitem[index]를 호출한다.
딕셔너리는 __iter__()가 구현되어 있기 때문에
for문에서 반복형(iterable)으로 작동할 수 있는 것이다.
number = {'one': 1, 'two': 2, 'three': 3}
number_iterater = iter(number)
print(number_iterater)
>>> <dict_keyiterator object at 0x1009610e0>
그리고 for문은 next함수에 그 반복자 객체를 넣어서
StopIteration예외가 나오기 전까지 실행하는 것이다.
while True:
try:
print(next(#이터레이터 객체))
except StopIteration:
break
for구문의 동작 과정은 생각보다 단순했다.
필자는 Sentence(iterable) 클래스가 iter() 함수를 통해
SentenceIterator(iterater) 클래스를 반환하면서
반복형(iterable)과 반복자(iterater)에 대해 설명했지만
클래스를 통해 반복자를 생성하는 방식은
파이썬스럽지 않다.
더욱더 파이썬스러운 방법을 소개하겠다.
제너레이터 vs 이터레이터
# 제너레이터 함수를 통한 반복자 생성
class Sentence:
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f'Sentence({self.text})'
def __iter__(self):
for i in self.words:
yield i
#------------------------------------------------------------------
# 클래스를 통한 반복자 생성
class Sentence:
def __init__(self, text):
self.text = text
self.words = text.split(' ')
def __repr__(self):
return f'Sentence({self.text})'
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self
놀랍게도 구분선을 기준으로 두 코드는 동일한 기능을 가지고
똑같이 동작한다.
딱 봐도 제너레이터 함수를 통한 반복자 생성이
훨씬 더 짧고 가독성도 좋다.
(훨씬 더 파이썬스럽지 않은가?)
원래는 __iter__() 매직 메서드로 우리가 따로 설정한 사용자 정의 이터레이터 클래스를 통해
이터레이터 객체를 생성해줬고
제너레이터 함수는 __iter__() 매직 메서드에 yield구문을 써주면서
제너레이터 함수로서 호출만 해도 제너레이터 객체를 반환한다.
<generator object Sentence.__iter__ at 0x100babf90> --> 제너레이터 객체
<__main__.SentenceIterator object at 0x100bd93a0> --> 이터레이터 객체
이 두 객체는 동일하게 동작한다.
그렇다면 제너레이터와 이터레이터는 같은 걸까?
https://stackoverflow.com/questions/2776829/difference-between-pythons-generators-and-iterators
Difference between Python's Generators and Iterators
What is the difference between iterators and generators? Some examples for when you would use each case would be helpful.
stackoverflow.com
스택오버플로우 알렉스 마르텔리의 답변에 의하면(첫 번째 답변)
제너레이터는 이터레이터의 정의를 충족하는 객체라고 한다.
(이터레이터의 프로토콜을 모두 구현한 것)
그렇기 때문에 모든 제너레이터 객체는 이터레이터라고 할 수 있지만
모든 이터레이터 객체는 제너레이터라고 할 수 없다고 한다.
우리는 사용자 정의 클래스를 통해 이터레이터 객체를 만들어줬다.
결국은 사용자 정의 클래스를 통해
이터레이터의 정의를 충족하는
이터레이터의 프로토콜을 구현한
이터레이터처럼 동작하는 객체를 만들어준 거다.
어찌 보면 덕 타이핑의 한 사례인 거 같다.
결론적으로 사용자 정의 클래스로 만든 이터레이터 객체는
더욱더 확장할 수 있다.
__iter__(), __next__() 외에도 더 많은 메서드를
구현할 수 있다.
그렇기 때문에
상태 유지 동작이 복잡한 클래스가 필요하거나
__next__(), __iter__(), __init__() 외에 다른 메서드들을
추가적으로 구현해야 할 때 사용자 정의 클래스로 이터레이터 객체를 만든다고 한다.
(대부분의 경우 제너레이터의 기능만으로도 충분하다고 한다)
하지만 굳이 차이점을 따지자면
객체를 생성하는 방법
또는 문법적인 부분이나
구조적인 부분을 따질 수 있다.
이런 부분에 대해 혹시나 궁금하면 이 글을 참고하기 바란다.
https://www.geeksforgeeks.org/difference-between-iterator-vs-generator/
Difference Between Iterator VS Generator - GeeksforGeeks
A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.
www.geeksforgeeks.org
제너레이터 함수
제너레이터 객체를 반환하는 제너레이터 함수를 보자
def gen_123():
yield 1
yield 2
yield 3
x = gen_123()
print(x)
>>> <generator object gen_123 at 0x104bbf510>
위 함수처럼
함수 본체 안에 yield구문이 있는 함수는
모두 제너레이터 함수이다.
일반 함수와의 차이점은
우리가 보통 일반 함수를 호출하게 되면
함수는 실행되고
결과값을 리턴한다.
하지만 제너레이터 함수는
함수를 호출했을 때 함수가 실행되지 않고
제너레이터 객체를 생성만 한다.
실제로 inspect모듈의 getgeneratorstate함수를 통해
제너레이터 객체의 상태를 보면
def gen_123():
yield 1
yield 2
yield 3
x = gen_123()
from inspect import getgeneratorstate
print(getgeneratorstate(x))
>>> GEN_CREATED
GEN_CREATED상태가 출력된다.
(GEN_CREATED상태는 실행 시작을 기다리는 상태다)
이제 이 객체를 실행하려면 어떻게 해야 할까?
답은 간단하다.
이터레이터 객체처럼
next()를 호출하면 된다.
def gen_123():
yield 1
yield 2
yield 3
x = gen_123()
print(next(x))
>>> 1
print(next(x))
>>> 2
print(next(x))
>>> 3
print(next(x))
>>> StopIteration
next() 함수를 호출할 때마다
다음 yield로 넘어가 오른쪽에 있는 값을 생성한다는 걸 확인할 수 있다.
그리고 마지막 yield에 도착한 상태에서
next() 함수를 호출하면
이터레이터와 같이 StopIteration에러가 발생한다.
제너레이터 함수를 보면
뭔가 yield를 기준으로 분리된 공간같이 느껴진다.
그리고 그 객체는 그 공간에서 자신이 어디쯤에 있는지
기억하고 있다.
(동작을 제어할 수 있다)
혹시나 병행성에 대해 알고 있는 사람이 있다면
이러한 제너레이터의 모습이
병행성 논리의 기반을 잘 보여주지 않나 싶다.
제너레이터 표현식
제너레이터 함수 말고도
제너레이터 표현식을 사용하면 제너레이터 객체를 반환할 수 있다.
(제너레이터 표현식은 편리 구문이다.)
# 지능형 리스트
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end.')
res1 = [x*3 for x in gen_AB()]
>>> start
>>> continue
>>> end.
print(res1)
>>> ['AAA', 'BBB']
for i in res1:
print('-->', i)
>>> AAA
>>> BBB
----------------------------------------------------------------------
# 제너레이터 표현식
res2 = (x*3 for x in gen_AB())
print(res2)
>>> <generator object <genexpr> at 0x104973510>
for i in res2:
print('-->', i)
>>> start
>>> AAA
>>> continue
>>> BBB
>>> end.
지능형 리스트 같은 경우는
gen_AB()로 호출한 제너레이터 객체를 바로 소비해서
리스트로 반환한다.
start, continue, end문이 바로 출력되는 걸 확인할 수 있다.
(이미 모든 next()가 StopIteration전까지 실행됨)
조금 더 간단한 예제로 보면
def gen_123():
yield 1
yield 2
yield 3
x = gen_123()
print(list(x))
>>> [1, 2, 3]
gen_123()을 호출해서 제너레이터 객체를 생성하고
바로 리스트형으로 변환하면
제너레이터 객체가 전부 다 소비된 채로
리스트에 담기는 걸 확인할 수 있다.
(위 예제와 같은 모습이다)
# 제너레이터 표현식
res2 = (x*3 for x in gen_AB())
print(res2)
>>> <generator object <genexpr> at 0x104973510>
for i in res2:
print('-->', i)
>>> start
>>> AAA
>>> continue
>>> BBB
>>> end.
하지만 제너레이터 표현식 같은 경우는
제너레이터 객체를 반환하기 때문에
우리가 예상한 대로 동작하는 걸 확인할 수 있다.
당연히 리스트 객체보다 제너레이터 객체를 사용할 수 있는 상황에선
제너레이터 객체가 공간 복잡도 측면에서 더 좋다.
(제너레이터 객체는 미리 모든 값을 생성하지 않기 때문에 --> 성급하지 않다.)
제너레이터 함수 vs 제너레이터 표현식
제너레이터 함수, 제너레이터 표현식
둘 다 결국은 제너레이터 객체를 반환하는데
어떤 방식을 사용하는 게 더 유리할까?
두 방법마다 장단점이 있다.
말 그대로 제너레이터 표현식은
'편리 구문'이다.
Fluent Python의 필자 루시아누 히말류에 의하면
제너레이터 표현식이 여러 줄에 걸쳐 있을 때는
가독성을 위해 제너레이터 함수를 사용하고
간단한 논리를 구현해야 할 때는
제너레이터 표현식을 사용한다고 한다.
(yield문이 있고 없고를 생각하면 어쩌면 당연한 말이다.)
마지막으로
itertools라는 모듈에는
다양하고 재미있게 조합할 수 있는 제너레이터 함수들이 있다.
그 함수들을 사용하면 아마 멋있는 제너레이터 객체들이 만들어질 것이다.
itertools모듈에 대해서는 다음 포스트에 작성해야겠다.
후기
예전에 제너레이터에 대해 배울 때
제너레이터는 그냥 메모리 줄여주는 애로 기억했었다.
사실 반복형과 반복자에 대해서도 잘 몰랐고
객체지향에도 무지했기 때문에
(지금도 무지하다)
제너레이터에 대해 알기는 사실상 쉽지 않았다.
이번 글을 쓰게 되면서
내가 너무 제너레이터에게 무심했다는 걸 알게 되었다.
(사실 제너레이터에게만 그랬던게 아닐 것이다.)
미안.. 제너레이터!
'Python' 카테고리의 다른 글
[파이썬] 코루틴 (0) | 2022.07.11 |
---|---|
[파이썬] 데코레이터 (0) | 2022.07.04 |
[파이썬]인터페이스, 덕타이핑, 프로토콜 (1) | 2022.06.25 |
[파이썬] 클로저(Closure) (0) | 2022.05.31 |
[파이썬] 함수가 가진 속성 파악하기 (0) | 2022.05.25 |