Fluent Python 14장 내용을 요약한 것입니다.
Sentence 클래스 구현하기
Iterable, Iterator, Generator를 쉽게 이해하기 위해서 Sentence
라는 예제 객체를 만들어볼 것이다.
Sentence 클래스는 문자열을 받아서 단어별로 나누어서 반복하는 클래스이다.
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
# instantiate 할 때 text를 받아서 문자열만 분리하여 리스트로 self.words에 저장
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
# index를 받으면 index에 해당하는 단어를 리턴
def __getitem__(self, index):
return self.words[index]
# 시퀀스 프로토콜에 완전히 따르려면 __len__을 구현해야하지만,
# iterable 객체에 필수적인 것은 아니다.
def __len__(self):
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
# Sentence 객체에 문자열을 전달하여 인스턴스를 하나 만든다.
s = Sentence('"The time has come," the Walrus said,')
# for문을 이용해서 각 요소를 돌면서 하나씩 print 한다.
for word in s:
print(word)
The
time
has
come
the
Walrus
said
Iter()
Sentence 객체의 인스턴스인 s
는 어떻게 for문으로 반복이 가능할까? Python 인터프리터가 어떤 객체 x
를 반복해야할 때는 항상 내장함수 iter(x)
를 호출한다.
iter() 함수는 다음의 과정을 수행한다.
- 객체가
__iter__()
메서드를 가지고 있으면, 이 메서드를 호출해서Iterator
객체를 가져온다. __iter__()
가 없지만__getitem__()
이 구현되어 있는 경우, 파이썬은 0번째에서 시작해서 항목을 차례로 가져오는 Iterator를 생성한다.__getitem__()
도 없는 경우TypeError: '클래스명' object is not iterable
이라는 메세지와 함께TypeError
가 발생한다.
모든 Sequence 객체는 __getitem__()
을 가지고 있으므로 Iterable 객체라고 할 수 있다. iter() 함수가 __getitem__()
까지 고려해주기 때문인데, 이는 하위호환성 때문이다.
* 객체가 Iterable 인지 체크하기
어떤 객체가 Iterable 객체인지 확인하는 가장 정확한 방법은 iter(x)
를 호출해서 TypeError 가 발생하는지 확인하는 것이다.
iter(x) 는 __getitem__()
까지 확인을 하는 반면, issubclass(x, abc.Iterable)
는 그렇지 않기 때문이다.
from collections import abc
s = Sentence('test test')
# Iterator 객체를 리턴함.
iter(s) # <iterator object at 0x0000016537225278>
# Sentence는 Iterable 객체이지만 issubclass 검사를 통과하지 못한다.
# __iter__() 가 없기 때문.
issubclass(Sentence, abc.Iterable) # False
Iterable 과 Iterator 의 정의
위에서 살펴본 내용을 바탕으로 Iterable 을 정의하면,
Iterable의 정의
- iter() 내장 함수가 Iterator 객체를 가져올 수 있는 모든 객체.
- Iterator 객체를 리턴하는
__iter__()
를 가지고 있는 객체.- 0부터 시작하는 인덱스를 받는
__getitem__()
를 가지고 있는 객체.
즉, 결국 인터프리터가 반복을 수행하기 위해서 필요한 것은 Iterator
객체이며, Iterable
객체는 인터프리터에게 Iterator
를 반환해주는 객체임을 알 수 있다.
그렇다면 Iterator
는 무엇일까?
다음은 문자열을 반복하는 간단한 for 루프이다.
s = 'ABC'
for char in s:
print(char)
A
B
C
이것을 while 문으로 똑같이 흉내내면 다음과 같다.
s = 'ABC' # string 도 Iterable 객체이다.
it = iter(s) # Iterator 객체를 생성한다.
while True:
try:
print(next(it)) # next 를 호출해서 다음 항목을 가져온다.
except StopIteration: # 더 이상 항목이 없으면 StopIteration 에러가 발생한다.
del it # it 참조 해제
break # 루프 정지
iter() 함수에 의해 반환된 Iterator 객체에는 next()
내장 함수를 사용하여 다음 항목을 반환하게 할 수 있으며, 다음 항목이 없을 경우 StopIteration 에러를 발생시킨다.
위 예에서 알아본 것 처럼 Iterator 객체의 표준 인터페이스는 다음의 메서드들을 정의한다.
- next() 내장 함수에 의해 호출되어 다음 항목을 리턴하는
__next__()
. 모든 항목을 다 반복한 경우 StopIteration 에러를 발생시킨다. - 자기 자신을 리턴하는
__iter__()
.
이를 바탕으로 Iterator를 정의해보면,
Iterator의 정의
- 다음 항목을 반환하거나, 다음 항목이 없을 때 StopIteration 을 발생시키는
__next__()
와 자기 자신을 리턴하는__iter__()
를 가지고 있는 객체.__iter__()
를 가지고 있으므로 Iterator 는 Iterable 이기도 하다.
* 객체가 Iterator 인지 체크하기
Python 3.4의 Lib/types.py
모듈 소스 코드에 다음과 같은 주석이 있다.
# Iterators in Python aren't a matter of type but of protocol. A large
# and changing number of builtin types implement *some* flavor of
# iterator. Don't check the type! Use hasattr to check for both
# "__iter__" and "__next__" attributes instead.
이에 따라 어떤 객체가 Iterator 객체인지 확인하는 가장 좋은 방법은 isinstance(x, abc.Iterator)
를 호출하는 것이다.
다음은 abc.Iterator 의 소스 코드이다.
class Iterator(Iterable):
__slot__ = ()
@abstractmethod
def __next__(self):
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C):
if cls is Iterator:
if (any("__next__" in B.__dict__ for B in C.__mro__) and
any("__iter__" in B.__dict__ for B in C.__mro__)):
return True
return NotImplemented
__subclasshook__()
는 isinstance()
또는 issubclass()
내장 함수가 호출될 때 실행되는 함수이다.
abc.Iterator 에 구현된 __subclasshook__()
는 검사 대상 클래스가 상속하는 모든 클래스들을 돌면서 __next__
와 __iter__
가 있는지 확인한다.
덕분에 직접 Iterator 객체를 상속받은 서브클래스들 뿐만 아니라, __next__()
와 __iter__()
를 구현하고 있는 가상 서브클래스들까지도 통과시켜준다.
from collections.abc import Iterator
class fakeIterator:
def __next__(self):
pass
def __iter__(self):
pass
f = fakeIterator()
isinstance(f, Iterator) # True
Sentence 클래스 V2: Iterator 버전
이제 Iterable 과 Iterator 에 대해서 자세히 알아보았으니, Iterable, Iterator 패턴에 맞춰서 Sentence 클래스를 개선시켜 보자.
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __getitem__(self, index):
return self.words[index]
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(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()
else:
self.index += 1
return word
def __iter__(self):
return self
SentenceIterator
는 자기 자신을 리턴하는 __iter__
와 다음 항목을 리턴하면서 다음 항목이 없는 경우 StopIteration 에러를 발생시키는 __next__
를 가지고 있으므로 Iterator 의 조건을 만족하였고,
Sentence 객체의 __iter__
는 SentenceIterator 라는 Iterator 객체를 리턴함으로써 Iterable 객체의 조건을 만족하였다.
Iterator 패턴에서 하면 안되는 것
Sentence 클래스 안에 __next__
를 구현해서 Iterable 이면서 동시에 Iterator 로 구현 하고 싶을 수 도 있다. 하지만 이는 전형적인 안티 패턴이다.
Iterator 객체에서 한 번 반환한 항목은 소진되어 버리기 때문에, 다시 항목을 반복하려면 새로운 Iterator 객체를 Iterable 객체로부터 받아와야 한다.
s = Sentence('test1, test2, test3')
it = iter(s)
next(it) # 'test1'
next(it) # 'test2'
print(list(it)) # ['test3']
it_2 = iter(s) # 새로운 iterator 객체 생성
print(list(it_2)) # ['test1', 'test2', 'test3']
디자인 패턴 책에 따르면 Iterator 패턴은 다음과 같은 용도로 사용되어야 한다.
- 집합 객체의 내부 표현을 노출시키지 않고 내용에 접근하는 경우
- 집합 객체의 다중 반복을 지원하는 경우
- 다양한 집합 구조체를 반복하기 위한 통일된 인터페이스를 제공하는 경우
다중 반복
을 지원하려면 Iterable 객체의 내부 상태는 Iterator 객체의 내부 상태와 독립적이어야 하며, 따라서 iter() 함수가 호출될 때마다 독립적인 새로운 Iterator 객체들이 새로 만들어져야 한다.
Sentence 클래스 V3: Generator 버전
이제 Iterator 패턴으로 구현된 Sentence 클래스를 generator
를 사용하여 좀 더 파이써닉하게 바꿔보자.
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for word in self.words:
yield word
return
Generator
어떤 함수 안에 yield
키워드가 있으면 그 함수는 generator
함수이다. generator 함수는 호출되면 generator 객체를 리턴한다. 즉, generator 함수는 generator 객체 공장이라고 할 수 있다.
def gen_123():
yield 1
yield 2
yield 3
print(gen_123)
print(gen_123())
<function gen_123 at 0x0000023D117B2E18>
<generator object gen_123 at 0x0000023D1372C0F8>
generator 객체에 next() 함수를 호출하면 함수 내에 있는 yield로 진행하고, 그 부분에서 값을 생성한다. 이것은 값을 return
하는 것과는 다르다. generator 함수가 return 되거나 끝까지 도달하여 끝나게 되면 StopIteration 이 발생한다.
g = gen_123()
next(g) # 1
next(g) # 2
next(g) # 3
next(g) # StopIteration!
다음은 generator 함수가 돌아가는 과정을 좀 더 자세히 알아볼 수 있는 예제이다.
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end.')
for c in gen_AB():
print('-->', c)
start # 1
--> A # 1
continue # 2
--> B # 2
end. # 3
for 문은 g = iter(gen_AB())
와 같은 문장을 실행해서 next(g)
를 실행한다.
-
첫 번째 next(g) 가 실행되면 첫 번째 yield 문이 있는 곳까지 코드가 실행된다. 따라서 ‘start’ 가 출력되고, 이어서 첫 번째 yield 문에 의해서 ‘A’ 가 출력된다. 이 ‘A’ 는 for 문의 c 변수에 할당되어서
print('-->',g )
에 의해 ‘–>’ 와 함께 출력된다. -
마찬가지로 두 번째 next(g) 가 실행되면 첫 번째 yield 문에서 시작하여 두 번째 yield 문까지 진행한다. ‘continue’ 가 출력되고, 이어서 ‘–> B’ 가 출력된다.
-
세 번째 next(g) 가 실행되면 두 번째 yield 문에서 이어서 진행된다. ‘end.’ 가 출력되고 yield 문이 더 이상 없으므로 함수가 return 되어 StopIteration 에러가 발생한다. for 문은 StopIteration 에러를 처리하여 에러 없이 함수가 종료된다.
지금까지 알아본 것 처럼 generator 함수는 Iterator 인터페이스를 구현하고 있는 generator 객체를 생성한다. 따라서 Sentence.__iter__()
에는 더 이상 별도의 Iterator 객체인 SentenceIterator 클래스가 필요없게 된다.
Sentence 클래스 V4: Lazy 한 구현
느긋한(Lazy) 구현이란 값의 생산을 가능한 한 최후의 순간까지 미루는 것을 말한다. 필요한 값을 필요한 그 때에 계산해내도록 하면 메모리를 줄일 뿐만 아니라 불필요한 처리도 피할 수 있다.
지금까지 작성한 Sentence 클래스는 느긋한 버전이 아니다. __init__()
에서 문자열을 받자마자 단어 단위로 쪼개서 self.words
에 저장하기 때문이다. 만약 매우 긴 문자열을 받는 다면 그 즉시 단어 단위로 쪼개는데 오랜 시간이 필요할 수 있고, 또 수 많은 단어들을 담고 있는 큰 리스트를 저장하고 있어야하기 때문에 메모리도 많이 사용하게 된다.
re.finditer()
함수는 re.findall()
의 느긋한 버전이다. next() 가 호출될 때마다 단어를 매치시켜 re.MatchObject
객체를 생성하는 generator 객체를 반환한다.
이것을 적용해보면 다음과 같다.
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text):
yield match.group()
__iter__()
함수는 이제 next() 가 호출될 그 때에 단어를 매치시켜서 그 결과로 나온 re.MatchObject
의 group() 값을 리턴하는 generator 객체를 리턴하는 generator 함수가 되었다.
Sentence V5: Generator 표현식
마지막으로 generator 함수를 더 간단히 만드는 방법인 generator 표현식
을 사용해서 Sentence 클래스를 개선해보자.
generator 표현식은 list 컴프리헨션과 비슷하게 생겼으며, []
대신 ()
를 사용한다.
다음 예제를 통해서 generator 표현식과 list 컴프리헨션의 차이를 알아보자.
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end.')
res1 = [x*3 for x in gen_AB()] # list 컴프리헨션
start
continue
end.
res1 변수가 생성되는 순간에 generator 함수 내부가 모두 실행되고, yield 문에서 생성된 값들이 x 에 할당되어 계산된 후 res1 변수에 리스트로 저장된다.
for i in res1:
print('--> ', i)
--> AAA
--> BBB
for 문을 통해 res1 변수를 돌면서 하나씩 프린트 한 결과.
반면 generator 표현식을 사용하면 다음과 같다.
res2 = (x*3 for x in gen_AB())
print(res2)
<generator object <genexpr> at 0x10063c240>
res2 는 generator 객체이다.
for i in res2:
print('--> ', i)
start
--> AAA
continue
--> BBB
end.
for 문으로 하나 씩 반복할 때 함수의 내부가 실행됨을 볼 수 있다.
generator 표현식을 적용한 Sentence 클래스는 다음과 같다.
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
generator 표현식은 짧은 generator 함수가 필요할 경우에 사용하면 가독성을 높이는데 도움이 된다.
Reference
Fluent Python Chapter 14 - Iterables, Iterators and Generators