본문 바로가기

Python

Python - Iterable / Iterator / Generator

이번에는 파이썬에서 iterable한 객체를 어떻게 다루는지와 함께 이와 관련된 개념들을 함께 살펴보도록 하겠습니다. :)

Iterable Obejct


Python Document에서는 iterable object의 의미를 객체에서 member 혹은 element를 하나씩 차례로 반환 가능한 object라고 정의한다.(쉽게 와닿을 것 같다.) Iterble object로는 sequence type인 list, string, tuple 등과 함께 non-sequence type인 dict나 __iter__()__getitem__() 매직메소드를 메소드로 가지고 있는 사용자 정의 클래스 등이 있다. 사실은 빌트인 타입들 또한 소스코드를 확인해보면 __iter____getitem__을 가지고 있다. 결국 이 두 매직메소드가 핵심인 것이다. 이러한 iterable object는 빌트인 함수인 iter()에 argument로 들어갈 때, 이 iterable obeject에 대한 iterator가 반환된다. 그렇다면 iterator란 무엇일까?

Iterator


파이썬에서 정의되는 Iterator는 next() 라는 빌트인 메소드를 통해서 데이터를 순차적으로 호출 가능한 object이다. 하지만, iterator는 iterable object와는 다르다. iterable한 object들이라고 해서 다 iterator는 아니다. 예를 들어 list를 살펴보자. 파이썬에서 list의 소스코드를 살펴보면( 파이썬은 Open Source Programming Language이기 때문에 Source Code를 확인할 수 있다.) 

class list(object): 
    """ 
    list() -> new empty list 
    list(iterable) -> new list initialized from iterable's items 
    """ 
   
    # ...
    
    def __iter__(self, *args, **kwargs): # real signature unknown 
        """ Implement iter(self). """ 
        pass 
 
  
    # ...

list의 소스코드를 살펴보면 위와 같이 __iter__라는 매직메소드를 list 클래스에서 가지고 있는 것을 볼 수 있다. 그렇기에 이 list는 iterable한 object라고 할 수 있다. 하지만 아무리 눈을 씻고 찾아봐도 __next__를 확인할 수 없다. 즉, next()메소드로 호출해도 동작하지 않는다는 것이다. 즉, list는 iterable object이지만, iterator는 아닌 것이다. 하지만, 이러한 iterator가 아닌 iterable object도 iter() 함수를 통해서 iterator로 만들 수 있다. iter()함수의 인자로 iterable object를 넣어주게 되면 iterator타입의 객체로 변경된다. 그렇게 되면 next()를 통해서 요소를 하나씩 꺼낼 수 있다는 것이다. 이를 확인해보기 위해서 임의의 리스트 a를 만든 후 dir()을 통해서 확인해보자. ( 각 객체가 가지고 있는 매직메소드는 dir()을 통해서 확인할 수 있다.)

a = [1,2,3]

dir(a)
>>> [ ... '__iter__', ...]

dir(iter(a))
>>> [... '__iter__', '__next__', ...]

그러면 위와같이 iter()함수를 통해 iterator 객체로 만들었을 때 __next__라는 매직메소드가 생겨서 next() 함수를 통해서 불러올 수 있는 객체가 된 것을 확인할 수 있다. 

💡Magic Method란? ( 간단히 )
파이썬에서는 객체에서 불러올 수 있는 메소드들 중에 __methodname__ 과 같이 양쪽으로 두개씩의 언더바가 붙어있는 메소드들이 있다. 이러한 메소드들을 매직메소드라고 하며, 실제로 프로그래머가 직접 사용하기보다는 객체에 어떤 액션을 취하느냐에 따라 클래스에서 내부적으로 처리되는 메소드들이다.
몇 가지 예를 들어보자. 우리가 어떤 int a와 b를 더하는 연산을 + operator를 통해서 한다고 해보자. 우리는 이때 a + b라는 + operator를 사용하는 액션을 취하고, 이에 의해서 a, b 객체 내부적으로 가지고 있는 매직메소드 __add__()가 처리되어 실제 덧셈 연산이 일어나는 것이다.

__next____iter__도 마찬가지다. iterable한 object인지 iterator인지 구분하기 위한 기준점이 __next__라고 이야기했고, 이러한 매직메소드는 next()라는 액션에 의해서 처리되는 것이다. 

여기까지 배웠다면, 한 가지 더 추가적으로 이러한 개념들이 쓰이면서 우리에게 무척 익숙한 개념을 짚고 넘어가려 한다. 바로 파이썬의 for loop이다. 파이썬의 for loop 문은 내부적으로 Iterator를 생성하여 동작한다. 예를 들어 List를 순회하는 for 문이라 하면, 해당 리스트의 Iterator를 생성한 뒤에 ( iter function이 해주는 역할과 비슷한 동작이 발생할 것이다.) __next__메서드를 이용해서 순회를 도는 방식인 것이다. 이러한 과정이 백그라운드에서 처리되고 우리에게는 값이 하나씩 가져와지는 것처럼 보이는 것이다.

https://realpython.com/python-for-loop/

그렇다면 우리는 본질적인 의문이 남는다. 도대체  iter() 함수가 어떻게 해주길래 iterable object를 iterator로 만드는 것일까? 물론 이에 대해서 간단히 __next__ 매직메소드가 생기는 것을 통해 간접적으로 이해해봤다. 하지만, 정확히 알기 위해서는 우리는 먼저 generator에 대해서 알아야 한다.

Generator


generator란,  iterator를 생성해주는 function으로 정의된다. ( 반드시 함수 안에 yield) 키워드를 사용한다. 이 점에서 일반적인 함수와 다르다.) 그러면 yield) 구문이 하는 역할이 무엇인지에 대해서 알아야 할 것이다. 이 yield 구문은 오직 generator function을 정의할 때만 사용된다. yield를 사용하게 되면 함수를 실행 중에 yield)구문을 만났을 때 해당 함수의 실행이 멈추며 yield)구문이 반환해주는 값을 next())를 호출한 쪽으로 전달해주게 된다. 이후 계속 실행되는 게 아니라 그대로 멈춰있게 된다.

https://codeburst.io/understanding-generators-in-es6-javascript-with-examples-6728834016d5?gi=8b7fa2ba518d

예시를 통해 확인해보자.

>>> def test_generator():
...     yield 1
...     yield 2
...     yield 3
... 
>>> gen = test_generator()
>>> type(gen)
<class 'generator'>
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3

이렇게 generator함수 내에서 yield)문을 만나면 반환해주는 값을 next)를 호출한 쪽으로 전달해주고, 그대로 멈춰있게 된다. 또 하나의 예시를 살펴보자. 이번에는 좀 더 그대로 멈춰있다가, 다시 호출될 때 이어서 한다는 것에 더 주목해보자.

def generator(n):
    i = 0
    
    while i < n:
        yield i
        i += 1
for x in generator(5):
	print x

>>> 0
>>> 1
>>> 2
>>> 3
>>> 4


출처: https://bluese05.tistory.com/56?category=559959

하나하나 따라가 보면 먼저 for문이 실행되면서 generator함수가 호출이 되고, generator함수 안에서 yield)문을 만나게 되었을 때 반환 값을 리턴해주고, (여기서는 i에 0을) generator함수가 종료되지 않고, 그대로 유지하고 있다가, x에 yield에서 전달한 값인 0을 전달한 후 다시 for문에 의해서 generator함수가 호출되면 이때 generator함수를 처음부터 시작하는 게 아닌 yield) 구문을 만나 멈췄던 그 이후부터 다시 시작되어서 i += 1)이 실행되고 다시 루프를 돌아서 다시 yield)문을 만나면 i에 1이라는 값을 전달하고 멈추게 된다. 이러한 과정이 반복되는 것이다.

여기서 우리는 앞서 for loop문이 사실은 iterable object를 iterator로 만들었던 점에 대해서 주목할 필요가 있다. 여기까지 배웠다면 느낌이 올 것이다. iterable object를 iterator로 만드는 그 내부적인 처리과정이 사실은 generator를 통해서 iterable object를 iterator로 만드는 과정이 있다는 것이다. 좋다, 내부적으로 이렇게 동작된다는 것을 알았다. 그런데 왜 굳이 for loop에서는 이런 과정을 거치는 것일까? 왜 이런 것을 사용하는 것일까? 

Why Generator ?

그 이유는 바로 메모리적인 효율성 때문이다.

일반적인 함수가 호출되면 코드의 첫 행부터 return)문이나 exception)문 혹은 마지막 구문을 만날 때까지 실행된다. 그 후 caller에게 모든 걸 반환해준다. 그리고 함수가 가지고 있던 모든 내부적인 함수나, 로컬 변수는 메모리상에서 지워지는 것이다. ( scoping rule )

그런데 프로그래머들은 어느 날 한 번에 일을 다하고 사라져 버리는 게 아닌, 하나의 일을 마치면 자기가 했던 일들을 기억하고 있으면서 대기하다가 다시 호출되었을 때 전에 하던 일을 이어서 하는 함수의 필요성을 느끼게 되었고, 그래서 사용된 게 generator다. 즉, generator를 사용하면, generator함수에서 사용된 local 변수나 내부 함수 같은 함수 안에서 사용된 데이터들이 메모리에 그대로 유지되었다가, 다시 호출할 때 이어서 사용하는 것이다. 이렇게 하게 되면 한꺼번에 메모리에 적재하는 게 아닌 차례대로 값에 접근할 때마다 메모리에 적재하기 때문에 큰 데이터를 다룰수록 효율적이게 된다.

조금 더 자세히 알아보기 위해서 list와 비교해보자. 사실 이 generator는 우리가 잘 아는 list comprehension 코딩 스타일을 generator를 사용하는 함수로 확장을 할 수도 있다. 이걸 generator comprehension 혹은 generator expression이라고 한다. 이때 list comprehension과는 다르게 [ ]) 대신 ( ))를 사용한다.

>>> [ i for i in xrange(10) if i % 2 ]
[1, 3, 5, 7, 9]
>>> ( i for i in xrange(10) if i % 2 )
<generator object <genexpr> at 0x7f6105d90960>

출처: https://bluese05.tistory.com/56?category=559959

그렇다면 다시 메모리의 관점으로 돌아와서 list와 generator object의 메모리를 비교해봄으로써, 실제로 메모리를 어떻게 효율적으로 이용하고 있는지를 확인해보자.

>>> import sys

>>> sys.getsizeof( [i for i in xrange(100) if i % 2] )    # list
536
>>> sys.getsizeof( [i for i in xrange(1000) if i % 2] )
4280

>>> sys.getsizeof( (i for i in xrange(100) if i % 2) )    # generator
80
>>> sys.getsizeof( (i for i in xrange(1000) if i % 2) )
80

출처: https://bluese05.tistory.com/56?category=559959

이렇게 list의 경우 사이즈가 커질수록 사용하는 총 메모리 사용량이 늘어나게 된다. 한꺼번에 적재해야 하기 때문이다. 하지만, generator의 경우 사이즈가 커진다고 해도 차지하는 메모리의 사이즈는 동일하다. next)가 호출되어 차례로 값에 접근할 때마다 메모리에 적재하는 방식이기 때문이다.

이렇게 대용량 데이터를 처리할 때 메모리 효율성으로 인해 generator를 많이 고려하게 된다. 또한, 파일 데이터를 처리할 경우 generator가 아주 많이 고려된다. 파일 데이터를 처리할 때 중간에 멈출 수도 있고, 전체가 아닌 필요한 일부를 가져올 수도 있기에 generator가 아주 많이 사용되는 부분이라고 한다.

 

지금까지 Iterable object, Iterator, Generator까지 살펴봤다. 이번 내용들을 거의 담고 있는 좋은 이미지가 있어서 가져와봤다. 정리하기에 아주 좋은 도식인 것 같다.

https://nvie.com/posts/iterators-vs-generators/

정리하자면 generator function이나 generator expression을 통해 generator object를 생성할 수 있고, 이러한 generator object는 항상 iterator로 사용될 수 있으며 ( 정의 자체가 iterator를 만들어내는 function이다.) 이 iterator로부터 next function을 통해서 lazily 하게 다음 값을 가져올 수 있고, 이러한 iterator는 항상 iterable한 객체이지만, iterable한 객체라고 해서 항상 iterator인 것은 아니고, next())를 통해서 값을 반환할 수 있어야 한다는 것이다. 이때 iterable object는 우리가 흔히 아는 객체에서 member를 하나씩 차례로 반환 가능한 object이다.

 

Reference

https://wikidocs.net/16069

 

39. Generator(제네레이터)

## 1. Generator란? - generator : iterator를 생성해주는 함수, 함수안에 yield 키워드를 사용함 - genrator 특징 - ite ...

wikidocs.net

https://fierycoding.tistory.com/47

 

파이썬 매직 메소드(던더 메소드)

더블언더스코어를 낀 메소드의 정체 파이썬에서는 객체에서 불러올 수 있는 메소드 중 __메소드이름__ 이런 식으로 더블언더스코어가 양쪽으로 붙어있는 메소드들이 있습니다. 이들은 매직 메

fierycoding.tistory.com

https://tibetsandfox.tistory.com/27

 

파이썬(python) - 이터레이터(Iterator)

이터레이터(Iterator)란? 이터레이터는 순서대로 다음 값을 리턴할 수 있는 객체를 의미합니다. 자체적으로 내장하고 있는 next 메소드를 통해 다음 값을 가져올 수 있습니다. 여기서 '순서대로'라

tibetsandfox.tistory.com

https://bluese05.tistory.com/56?category=559959 

 

python generator(제너레이터) 란 무엇인가

Python Generator  먼저 python docs 의 generator 에 대한 정의를 보자. generator A function which returns an iterator. It looks like a normal function except that it contains yield statements for pr..

bluese05.tistory.com

https://schoolofweb.net/blog/posts/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0-generator/

 

파이썬 – 제너레이터 (Generator) - schoolofweb.net

안녕하세요. 스쿨오브웹의 이상희입니다. 지난 파이썬 강좌인 파이썬 – 데코레이터 (Decorator)에 이어서 이번 강좌에서는 파이썬의 제너레이터에 대해서 알아보고자 합니다. 프로그램을 조금이

schoolofweb.net

 

'Python' 카테고리의 다른 글

Python - Decorator  (0) 2022.02.02
Python - Variable Length Arguments  (0) 2022.02.02
Python Scoping Rule ( by LEGB rule )  (0) 2022.02.01
Python - Call by Object Reference  (0) 2022.02.01
Python Dynamic Typing  (0) 2022.01.28