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()를 호출할 때마다 두 가지 중요 작업이 수행된다.

  1. 다음 next() 호출을 위해 상태 변경
  2. 현재 호출에 대한 결괏값 생성

핵심 아이디어: 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의 타입

  1. generator function
    • yield가 사용되는 모든 함수이다.
  2. 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는 끔찍하게 느리기 때문이다.


References

댓글남기기