이터레이터와 제너레이터
병행성 흐름을 이해하기 위해서 연결된 개념인 이터레이터와 제너레이터를 좀 더 깊이 이해할 필요가 있다.
이터레이터는 반복이 가능한 객체를 의미한다. 파이썬에서 기본적으로 반복이 가능한(iterable) 자료형들은 다음과 같다.
collections, list, text file, Dict, Set, Tuple, unpacking, *args…
__iter__의 구현방식
반복가능한 객체는 dir()로 확인 시 iter 메서드를 포함하고 있다. 다시 말해 iter() 함수를 내부적으로 호출한다는 것이다. 확인을 위해서 직접 반복 가능한 문자열 객체를 iter() 함수에 대입해보자. iter()함수를 문자열 w에 적용하고, 적용된 t 변수에서 dir(t) 의 출력 시, __next__ 가 출력되는 것을 확인할 수 있다. next(t)를 print 해보면 문자열이 하나씩 출력된다. 이것은 iter를 구현하는 class가 내부적으로 다음에 출력할 값을 저장하고 있다는 것을 의미한다. 만약 w 문자열의 길이 이상으로 next를 호출하면 StopIteration exception이 발생한다.
w = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
print(dir(w))
#['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
t = iter(w)
print(dir(t))
#['__class__', ... '__next__', ...]
print(next(t)) #A
print(next(t)) #B
캡슐화 이해
우리가 for문을 사용하여 Iterable한 클래스를 반복하는 것은 이 next와 while문 등을 활용하여 아래와 같이 구현했기 때문이다. 사용자가 구체적인 내용을 알지 않아도 되도록 캡슐화된 것이라 할 수 있다.
for c in w:
print(c)
#동치
while True:
try:
print(next(w))
except StopIteration:
break
정리하자면 반복이 가능한 iterator는 __iter__와 __next__ 메서드를 구현하고 있다. 그리고 이를 편리하게 사용할 수 있도록 파이썬은 iterator 객체를 for문에 전달하면 내부적으로 next를 호출하고, StopIteration 예외처리를 해주는 것이다.
hasattr, isinstance
추가적으로 클래스가 어떤 메서드를 구현하고 있는지 확인하기 위해서 dir 메서드 외에 hasattr, isinstance 메서드를 활용할 수 있다. dir() 메서드로 직접 출력하는 것에 비해 결과를 바로 확인할 수 있다. isinstance 메서드는 collections에서 import 받아야 한다.
from collections import isinstance
print(hasattr(t, '__iter__')) #True
print(isinstance(t, Iterable)) #True
예시
일반 클래스를 위에서 배운 내용을 활용해서 iterable 하게 만든다. 처음엔 iter 메서드를 오버라이드 해보고, 더 나은 방식인 제너레이터를 활용하는 방식으로 구현해본다.
__next__메서드를 오버라이드하여 단어를 입력받아 split하고, 리스트값으로 갖고 있는 iterable한 객체를 생성한다.
class WordSplitter:
def __init__(self, text):
self._idx = 0
self._text = text.split(' ')
def __next__(self):
try:
word = self._text[self._idx]
except IndexError:
raise StopIteration('Stopped Iteration ^^')
self._idx += 1
return word
def __repr__(self):
return 'WordSplit(%s)' % self._text
wi = WordSplitter('Do what you want no matter how old you are')
print(wi)
print(next(wi))
print(next(wi))
print(next(wi))
제너레이터 패턴
제너레이터 패턴을 이용하여 위 코드를 개선해본다. 앞서 학습했듯이 제너레이터를 이용하면 작은 메모리 조각을 사용하기 때문에 메모리 사용량이 줄어든다. 게다가 이후 살펴볼 코루틴(Coroutine) 구현과 연동할 수 있으므로 반드시 잘 익혀두어야 한다.
class WordSplitGenerator:
def __init__(self, text):
self._text = text.split(' ')
def __iter__(self):
for word in self._text:
yield word
return
def __repr__(self):
return 'WordSplit(%s)' % self._text
wg = WordSplitGenerator('Do what you want no matter how old you are')
wt = iter(wg)
print(next(wt)) # Do
따로 예외처리나 index 정보를 기억할 필요가 없이 yield 키워드만 잘 적용하면 된다.
병행성과 Yield
병행성과 병렬성
- 병행성(Concurrency) : 한 컴퓨터가 여러 일을 동시에 수행
- 병렬성(Parallelism) : 여러 컴퓨터가 여러 작업을 동시에 수행
Yield 구문
병행성 작업을 수행하는 제너레이터를 생성해보자. yield 구문을 넣으면 그 함수는 제너레이터 취급한다. 일반 함수는 호출 시 함수 내부에 작성한 코드가 실행되지만, 이 함수는 호출하면 코드가 실행되는 것이 아니라 generator 객체가 반환된다. 그리고 제너레이터는 iter() 메서드로 iterable 하게 만들 수 있다.
yield는 반복자의 중단 또는 시작 지점이 된다. next를 사용하면 각 yield가 있는 라인까지만 실행된다. 만약 for문을 통해 반복하면 yield가 있는 라인의 이전 라인까지만 실행된다.
# Generator Ex1
def generator_ex1():
print('Start')
yield 'A Point'
print('Continue')
yield 'B Point'
print('End')
temp = iter(generator_ex1())
print(temp) # <generator object generator_ex1 at 0x4002000ac0>
print('--')
print(next(temp))
# Start
# A Point
print('--')
print(next(temp))
# Continue
# B Point
print('--')
print(next(temp))
# End
# *StopIteration Error occurred*
'''
Start
A Point
--
Continue
B Point
--
End
*StopIteration Error*
'''
for v in generator_ex1():
print('---')
print(v)
'''
Start
---
A Point
Continue
---
B Point
End
'''
dir() 메서드를 적용해보면, temp나 generator_ex1 모두 __iter__와 __next__를 포함하고 있다. 그러나 iter()를 적용한 temp만 next() 호출 시 이전 값을 기억해서 반복해서 호출 시 순차적으로 A Point, B Point 식으로 출력된다. 그냥 generator_ex1() 자체에 next()를 걸면 A Point 까지만 반복해서 호출되고 더 이상 진행이 안된다.
generator로 반복을 할 때는 for문으로 실행해야 정상적인 동작을 기대할 수 있다. for문으로 실행 시에는 StopIteration이 나오는 부분에서 예외처리를 하여 중단하는 로직이 포함되기 때문에 그 이전까지만 실행되고 에러는 발생하지 않는다.
Yield와 제너레이터의 특징
1. Comprehension 문법
comprehension 문법으로 반복하면, 반복인자가 yield 라인이 되어 반복된다. List Comprehension인 경우 yield가 적용된 구문만 반복되고, Generator Comprehension인 경우 generator 자체가 출력된다. 여기에 for문을 적용하면 반복되는 yield문 외에 다른 코드들도 실행되는 것을 볼 수 있다.
temp2 = [x * 3 for x in generator_ex1()]
temp3 = (x * 3 for x in generator_ex1())
print('temp2----')
print(temp2)
print('temp3----')
print(temp3)
'''
temp2----
['A PointA PointA Point', 'B PointB PointB Point']
temp3----
<generator object <genexpr> at 0x400206e970>
'''
for i in temp3:
print(i)
'''
Start
A PointA PointA Point
Continue
B PointB PointB Point
End
'''
2. 메모리 분할 적용 확인
일반 return 문을 통해서 iterable한 문자열을 출력하면, 모든 자료가 list에 append된 후에 적용되기 때문에 3초후 A, B, C가 한번에 출력된다. alphabets 자유 변수에 1초에 하나씩 담고, for문으로 빠르게 반복해서 출력하는 것이다. 그러나 yield문을 통해서 문자열을 출력하면 1초마다 하나씩 출력되는 것을 확인할 수 있다.
import time
#3초후 A B C 한번에 출력
def return_abc():
alphabets = []
for ch in "ABC":
time.sleep(1)
alphabets.append(ch)
return alphabets
for ch in return_abc():
print(ch)
#1초마다 순차적으로 A B C 하나씩 출력
def yield_abc():
for ch in "ABC":
time.sleep(1)
yield ch
for ch in yield_abc():
print(ch)
for문에 breakpoint를 걸고 디버깅을 해보면, time.sleep(5)에 따라 5초에 한번씩 결과가 들어와서 순차대로 A, B, C가 각각 출력되는 것을 확인할 수 있다. 즉 미리 A,B,C를 받는 것이 아니라 하나씩 받아서 곧바로 관련된 로직을 실행하는 것이다.
3. 무한 생성
본래 리스트와 같이 메모리 공간을 계속 잡아먹는 구조에서는 무한 생성이 불가하다. 하지만 제너레이터 방식을 이용하면 이론적으로는 무한 생성이 가능하다.
다만 이렇게 실행 시, IDE가 제대로 컨트롤할 수 없다면 OS에서 cpu 부하를 막기 위해 중단시킨다. 멈추고 싶다면 Pycharm의 실행 종료 버튼을 누르거나 윈도우의 경우 Ctrl + C 버튼을 누르면 된다고 한다.
def yield_infinite_abc():
while True:
yield "A"
yield "B"
yield "C"
for ch in yield_infinite_abc():
print(ch)
예외처리
참고 3의 예제를 살펴보자. next(k)를 호출했을 때 0으로 나누어 exception이 발생하면 generato는 예외를 전달한다. 다시 말해 제너레이터인 g() 함수가 제너레이터를 호출한 caller에게 exception을 전달하는 것이다. 이처럼 한 번 예외가 발생한 제너레이터는 next()를 통해서 다시 호출할 수 없다.
>>> def f():
return 1/0
>>> def g():
yield f() # the zero division exception propagates
yield 42 # and we'll never get here
>>> k = g()
>>> next(k)
ZeroDivisionError: integer division or modulo by zero
>>> next(k) # and the generator cannot be resumed
StopIteration
itertools
제너레이터를 활용한 무한 자료 생성을 itertools를 활용하여 더 간편하게 할 수 있다.
import itertools
#count : 무한 제너레이터
gen1 = itertools.**count**(1, 2.5)
print(next(gen1))
print(next(gen1))
print(next(gen1))
print(next(gen1))
'''
1
3.5
6.0
8.5
'''
#금지 : OS가 IDE 중지시킴
while True:
print(next(gen1))
주요 메서드
기타 itertools에서 제공하는 주요 메서드들을 알아보자.
# takewhile : 범위 제한
gen2 = itertools.takewhile(lambda n: n < 1000, itertools.count(1, 4))
for v in gen2:
print(v) # 1,5,9,...997
# pass
# filterfalse:필터 반대
gen3 = itertools.filterfalse(lambda n: n < 3, [1, 2, 3, 4, 5])
for v in gen3:
print(v) # 3,4,5
# accumulate: 누적 합계
gen4 = itertools.accumulate([x for x in range(1, 101)])
for v in gen4:
print(v) # 1, 3, 6, 10, ..., 5050
# 연결1
gen5 = itertools.chain("ABCDE", range(1, 11))
print(list(gen5))
# ['A', 'B', 'C', 'D', 'E', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 연결2
gen6 = itertools.chain(enumerate("ABCDE"))
print(list(gen6))
# [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D'), (4, 'E')]
# 개별
gen7 = itertools.product("ABCDE")
print(list(gen7))
# [('A',), ('B',), ('C',), ('D',), ('E',)]
# 개별2 : 경우의 수를 튜플로 만들어준다!
gen8 = itertools.product("ABCDE", repeat=2)
print(list(gen8))
# [('A', 'A'), ('A', 'B'), ('A', 'C'), ('A', 'D'), ('A', 'E'), ('B', 'A'), ..., ('E', 'E')]
# 그룹화
gen9 = itertools.groupby("AAABBCCCCDDEEEE")
# print(list(gen9))
# [('A', <itertools._grouper object at 0x4002192730>), ('B', <itertools._grouper object at 0x4002192670>), ... , ('E', <itertools._grouper object at 0x40021928b0>)]
for chr, group in gen9:
print(chr, " : ", list(group))
"""
A : ['A', 'A', 'A']
B : ['B', 'B']
C : ['C', 'C', 'C', 'C']
D : ['D', 'D']
E : ['E', 'E', 'E', 'E']
"""
참조
1. 인프런 강의 - 우리를 위한 프로그래밍 : 파이썬 중급 (Inflearn Original)
2. daleseo.com - 파이썬의 yield 키워드와 제너레이터
https://www.daleseo.com/python-yield/
3. AllaboutIoT - Python 비동기 프로그래밍 제대로 이해하기(1/2) - Asyncio, Coroutine
https://blog.humminglab.io/posts/python-coroutine-programming-1/#iterator
'Programming-[Backend] > Python' 카테고리의 다른 글
파이썬 중급 - 9. 동시성과 병렬성 : Futures (0) | 2022.07.31 |
---|---|
파이썬 중급 - 8. 제너레이터 개념 되짚기, 코루틴 이해하기 (0) | 2022.07.24 |
파이썬 중급 - 6. 고위 함수(Higher Order Function), 클로저(Closure) 기본 (0) | 2022.07.19 |
파이썬 중급 - 5. 고위 함수(Higher Order Function), 클로저(Closure) 기본 (0) | 2022.07.17 |
파이썬 중급 - 4. 해시테이블(Dictionary), Set (0) | 2022.07.17 |