pythonic tools - iterable, iterator, generator
itertools 학습을 위해 iterable, iterator, generator를 먼저 정리했다.
iterable
iterable은 반드시 데이터 구조일 필요는 없고(그럴 수도 있지만), member를 반환할 수 있는 모든 객체(object)가 가능하다. list, str, tuple, dict, file 등이 해당된다.
>>> x = { 'a': 1, 'b' : 2, 'c': 3 }
>>>
>>> for y in x:
... print y
...
a
c
b
또한, __iter__() 나 __getitem__() 메소드로 정의된 class는 모두 iterable 하다고 할 수 있다.
iterator
iterator는 next()로 데이터를 순차적으로 호출 가능한 객체이다. 만약 next()로 다음 데이터를 불러올 수 없을 경우(다 뽑아서 없다거나), StopIteration exception 발생.
>>> x = [1, 2, 3] # iterable, 이 자체는 iterator가 아님.
>>> y = iter(x) # iterator의 인스턴스
>>> z = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> next(z)
1
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>
피보나치 수를 생성하는 iterator 예시
>>> class fib:
... def __init__(self):
... self.prev = 0
... self.curr = 1
...
... def __iter__(self):
... return self
...
... def __next__(self):
... value = self.curr
... self.curr += self.prev
... self.prev = value
... return value
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
위 클래스는 iter 메소드와 next 메소드를 사용하므로 iterable이자 iterator이다. next()를 호출할 때마다 두 가지 중요 작업이 수행된다.
- 다음 next() 호출을 위해 상태 변경
- 현재 호출에 대한 결괏값 생성
핵심 아이디어: a lazy factory
- iterator는 값을 요청할 때 까지 계산을 수행하지는 않는다. 값을 요청할 때만 계산을 수행하고, 다시 쉬는 상태로 돌아간다. 마치 텐서플로우의 그것?
generator
특별한 종류의 iterator이다.
- 모든 generator는 iterator (역은 성립하지 않는다)
- 모든 generator는 lazy factory (값을 그때 그때 생성)
generator로 작성된 피보나치 수 생성 함수
>>> def fib():
... prev, curr = 0, 1
... while True:
... yield curr
# return 대신 yield를 이용하여 값을 하나씩 던져 준다.
... prev, curr = curr, prev + curr
...
>>> f = fib() # 이 상태에서는 계산은 하나도 실행되지 않는다.
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>> list(islice(f, 0, 10))
[89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
islice를 씌워 iterator화 시키고(이때도 아직 계산 x), list를 씌움으로써 마침내 계산이 실행된다. islice에서 10번째까지만 반환을 요청했으므로 11번째 next()는 generator에 도달하지 않는다. 그리고 다시 한번 f
를 같은 방식으로 실행하면 11번째 값부터 차례로 값을 던지게 된다.(그 전에 값들은 이미 던져서 사라짐)
generator의 타입
- generator function
- yield가 사용되는 모든 함수이다.
- generator expression
- 함수형 외의 generator는 list comprehension으로 표현된 것과 같다.
예시) 제곱수의 리스트를 생성하는 구문
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> [x * x for x in numbers]
[1, 4, 9, 16, 25, 36]
동일 작업을 set comprehension으로 표현
>>> {x * x for x in numbers}
{1, 4, 36, 9, 16, 25}
동일 작업을 dict comprehension으로 표현
>>> {x: x * x for x in numbers}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}
generator expression으로 표현
- tuple comprehension이 아니다.
>>> lazy_squares = (x * x for x in numbers)
>>> lazy_squares
<generator object <genexpr> at 0x10d1f5510>
>>> next(lazy_squares)
1
>>> list(lazy_squares)
[4, 9, 16, 25, 36]
generator 똑똑하게 사용하기
generator를 통해 조금 더 pythonic한 코드를 생산해보자. 바뀐 코드는 길이가 짧을 뿐만 아니라 메모리 및 CPU 효율이 좋다.
def something():
result = []
for ... in ...:
result.append(x)
return result
위 코드를 다음으로 교체
def iter_something():
for ... in ...:
yield x
# def something() # 정말로 리스트 구조가 필요할때만
# return list(iter_something())
리스트 변수가 없어도 되는 상황이라면 위와 같이 고치는 것이 당연 효율적으로 보인다. 메모리 절약도 절약이지만, 내가 알기로 python의 list.append는 끔찍하게 느리기 때문이다.
Leave a comment