본문 바로가기

컴퓨팅

Python_generator

결과만 두고 보면 'for제어문'과 같다고 보는게 맞다. 

-서로 동일하게 보되, generator은 객체로서 사용 라이브러리에 따라 Return값이 다를 수 있음만 인지

<for..generator> 와 <for..in> 은 서로 같다라고 생각하고 출발하는 것이 이해하기 쉽다.

def generator(n):
    i = 0
    while i < n:
        yield i
        i += 1

for x in generator(5):
    print(x)

for x2 in range(5):
    print(x2)

print(x,x2)

 

두 제어문 모두 1,2,3,4를 출력하며

마지막 메모리에 있는 값도 x=x2=4 동일하다.

 

그럼 왜 generator를 쓰는 것인가.

눈으로 직접 확인 가능한 분명한 차이

- 기본적인 제어문에서는 in 다음에는 '배열'구조만 받는다.

- 그러나 generator은 객체로서 다르게 함수나 다른 자료구조와 결합시켜 확장이 가능하다. 

ex1.) generator 객체로부터 2개 인자를 반복 리턴 

#gen_func()이라는 genrator 객체를 만들었다고 가정
for train_index, test_index in gen_func(examples):
	x_train, x_test = features[train_index], features[test_index]

 

*참고로 generator 자료구조 형태의 generator객체를 만드는 방법은 간단하다

>>> [ 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>

 ex2.) generator 객체는 쉽게 반복 제어기능을 지원

generator-yield

def fibonacci_func(n):

	a,b = 0, 1
	i = 0

	while True:
		if (i > n): return
        
        yield a
        a, b = b, a+b
        i += 1

yield 는 generator가 반복연산이 가능하게끔 한다. 일반 함수와 구분되는 가장 핵심적인 부분이다. 

 

먼저, 일반적인 함수의 경우를 생각해보자. 

일반적인 함수는 사용이 종료되면 결과값을 호출부로 반환 후 함수 자체를 종료시킨 후 메모리 상에서 클리어 된다.

 

하지만, yield 를 사용할 경우는 다르다.

 generator 함수가 실행 중 yield 를 만날 경우, 해당 함수는 그 상태로 정지 되며, 반환 값을 next() 를 호출한 쪽으로 전달 하게 된다. 이후 해당 함수는 일반적인 경우 처럼 종료되는 것이 아니라 그 상태로 유지되게 된다. 즉, 함수에서 사용된 local 변수나 instruction pointer 등과 같은 함수 내부에서 사용된 데이터들이 메모리에 그대로 유지되는 것이다. 

 

좀 더 쉬운 코드로 보자면 아래와 같다.

def generator(n):
    i = 0
    while i < n:
        yield i
        i += 1

for x in generator(5):
	print x
 
#1~4까지 차례로 출력

위 구문을 하나하나 살펴보자.

 

  1. for 문이 실행되며, 먼저 generator 함수가 호출된다.
  2. generator 함수는 일반 함수와 동일한 절차로 실행된다. 
  3. 실행 중 while 문 안에서 yield 를 만나게 된다. 그러면 return 과 비슷하게 함수를 호출했던 구문으로 반환하게 된다. 여기서는 첫번재 i 값인 0 을 반환하게 된다. 하지만 반환 하였다고 generator 함수가 종료되는 것이 아니라 그대로 유지한 상태이다.
  4. x 값에는 yield 에서 전달 된 0 값이 저장된 후 print 된다. 그 후 for 문에 의해 다시 generator 함수가 호출된다. 
  5. 이때는 generator 함수가 처음부터 시작되는게 아니라 yield 이후 구문부터 시작되게 된다. 따라서 i += 1 구문이 실행되고 i 값은 1로 증가한다.
  6. 아직 while 문 내부이기 때문에 yield 구문을 만나 i 값인 1이 전달 된다.
  7. x 값은 1을 전달 받고 print 된다. (이후 반복)

눈에 보이지 않는 차이

메모리기능 

>>> 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

 list 의 경우 사이즈가 커질 수록 그만큼 메모리 사용량이 늘어나게 된다. 하지만, generator 의 경우는 사이즈가 커진다 해도 차지하는 메모리 사이즈는 동일하다. 이는 list 와 generator의 동작 방식의 차이에 기인한다.

 

list 는 list 안에 속한 모든 데이터를 메모리에 적재하기 때문에 list의 크기 만큼 차지하는 메모리 사이즈가 늘어나게 된다. 하지만 generator 의 경우 데이터 값을 한꺼번에 메모리에 적재 하는 것이 아니라 next() 메소드를 통해 차례로 값에 접근할 때마다 메모리에 적재하는 방식이다. 

연산 차이 in comprehension

def sleep_func(x):
	print("sleep...")
	time.sleep(1)
	return x

# list 생성
list = [sleep_func(x) for x in range(5)]

for i in list:
	print i

<결과>
sleep...
sleep...
sleep...
sleep...
sleep...

0
1
2
3
4


# generator 생성
gen = (sleep_func(x) for x in range(5))

for i in gen:
	print i

<결과>
sleep...
0
sleep...
1
sleep...
2
sleep...
3
sleep...
4

 위 결과 값을 보면, generator 를 사용하였을 경우 어떤 차이가 있는지 알것이다. list 의 경우 list comprehension 을 수행 할때, list의 모든 값을 먼저 수행하기 때문에 sleep_func() 함수를 xrange() 값 만큼 한번에 수행하게 된다. 만약 sleep_func() 에서 수행하는 시간이 길거나 list 값이 매우 큰 경우 처음 수행 할때 그만큼 부담으로 작용된다.

 

 하지만 generator 의 경우 generator 를 생성할 때는 실제 값을 로딩하지 않고, for 문이 수행 될때 하나씩 sleep_func()을 수행하며 값을 불러오게 된다. 수행 시간이 긴 연산을 필요한 순간까지 늦출 수 있다는 점이 특징이다.

 

 

 



출처: https://bluese05.tistory.com/56 [ㅍㅍㅋㄷ]

 

'컴퓨팅' 카테고리의 다른 글

관계형 데이터 모델링_모든 내용  (0) 2021.01.31
Pandas_DataFrame _processing_Tech  (0) 2021.01.18
카디널리티란  (0) 2021.01.16
Anaconda_prompt  (0) 2021.01.16
python_English to Korean  (0) 2021.01.15