인공지능/PYTHON

Python - generator

bibibig_data 2021. 6. 17. 17:31

     - https://mingrammer.com/introduce-comprehension-of-python/
     - https://stackoverflow.com/questions/16940293/why-is-there-no-tuple-comprehension-in-python
     - http://schoolofweb.net/blog/posts/파이썬-제너레이터-generator/

 

# generator.py
def square_numbers(nums):
    result = []
    for i in nums:
        result.append(i * i)
    return result

my_nums = square_numbers([1, 2, 3, 4, 5])

print my_nums

아주 간단한 함수를 정의하고 호출하는 코드입니다. 정의된 함수는 인자로 받은 리스트를 for 루프로 돌면서 i * i의 결과값으로 새로운 리스트를 만들고 리턴하는 함수입니다.

터미널이나 커맨드창을 여신 후, generator.py 파일이 저장된 위치로 이동하여 주십시오. 이동을 하셨으면 다음의 명령어로 프로그램을 실행하여 주십시오.

$ python generator.py
[1, 4, 9, 16, 25]

새로운 리스트가 결과값으로 리턴되었습니다.

이 코드를 제너레이터로 만들어 보겠습니다.

#generator.py
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])  #1

print my_nums
$ python generator.py
<generator object square_numbers at 0x1007c8f50>

제너레이터라는 오브젝트가 리턴 됐습니다. 제너레이터는 자신이 리턴할 모든 값을 메모리에 저장하지 않기 때문에 조금 전 일반 함수의 결과와 같이 한번에 리스트로 보이지 않는 것입니다. 제너레이터는 한 번 호출될때마다 하나의 값만을 전달(yield)합니다. 즉, 위의 #1까지는 아직 아무런 계산을 하지 않고 누군가가 다음 값에 대해서 물어보기를 기다리고 있는 상태입니다. 확인해 볼까요?

#generator.py
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])

print next(my_nums)
$ python generator.py
1

next()함수를 이용하여 다음 값이 무엇인지 물어봤습니다. 다음 값은 1이라고 하네요. 이번에는 몇번 더 물어보겠습니다.

generator.py
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])

print next(my_nums)
print next(my_nums)
print next(my_nums)
print next(my_nums)
print next(my_nums)
$ python generator.py
1
4
9
16
25

위의 첫 번째 예제의 일반 함수가 리턴한 리스트의 값이 모두 출력되었습니다. 그런데 여기서 한번더 next() 함수를 호출하면 어찌 될까요? 한번 해보죠.

 

#generator.py
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])

print next(my_nums)
print next(my_nums)
print next(my_nums)
print next(my_nums)
print next(my_nums)
print next(my_nums)
#$ python generator.py
1
4
9
16
25
Traceback (most recent call last):
  File "generator.py", line 12, in 
    print next(my_nums)
StopIteration

StopIteration 예외가 발생하였습니다. 더 이상 전달할 값이 없다는 뜻입니다.

제너레이터는 일반적으로 for 루프를 통해서 호출하여 사용하는데 그 예를 한번 보시죠.

#generator.py
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])

for num in my_nums:
    print num
$ python generator.py
1
4
9
16
25

이번에는 모든 값이 출력되고 StopIteration 예외는 발생하지 않았습니다. for 루프는 자신이 어디서 멈춰야 하는지를 알고 있기 때문입니다.

여기서 한 가지 제너레이터가 일반함수 보다 좋은점을 지적할 수가 있습니다. 그건 코드가 더 단순하다는 겁니다. 파이썬의 철학이 담긴 "The Zen of Python"의 3번째 항목에 이런 글이 적혀있습니다. "복잡한것 보다는 단순한 것이 좋다." 그렇습니다. 이왕이면 복잡한 코드 보다는 단순한 코드가 더 좋은겁니다.

그런데 "list comprehension"을 사용하면 위의 코드보다 더 간단한 코드를 만들 수가 있습니다. 예제를 보겠습니다.

generator.py
my_nums = [x*x for x in [1, 2, 3, 4, 5]]

print my_nums

for num in my_nums:
    print num
$ python generator.py
[1, 4, 9, 16, 25]
1
4
9
16
25

첫 번째 예제의 일반 함수와 같은 리스트를 리턴하는군요. 같은 구문을 조그만 바꾸면 제너레이터를 만들 수가 있습니다.

generator.py
my_nums = (x*x for x in [1, 2, 3, 4, 5])  #1

print my_nums

for num in my_nums:
    print num

#1의 "[]"를 "()"로 바꾸니 제너레이터가 생성됐습니다. 간단하네요. ㅎ

그런데 for 루프를 사용하지 않고 한번에 제너레이터 데이터를 보고 싶으면 어떻게 할까요? 그럴때는 제너레이터를 간단히 리스트로 변환하면 됩니다.

#generator.py
# -*- coding: utf-8 -*-

my_nums = (x*x for x in [1, 2, 3, 4, 5])  # 제너레이터 생성

print my_nums
print list(my_nums)  # 제너레이터를 리스트로 변형
$ python generator.py
<generator object <genexpr> at 0x1006c8f50>
[1, 4, 9, 16, 25]

간단히 리스트로 변형되어 출력 되었습니다. 여기서 한 가지 주의하셔야 하는 점은 한 번 리스트로 변형하면 제너레이터가 가지고 있던 장점을 모두 잃게 된다는 점입니다. 이 장점 중 가장 중요한 것은 퍼포먼스 입니다. 위에서도 설명하였듯이 제너레이터는 모든 결과값을 메모리에 저장하지 않기 때문에 더 좋은 퍼포먼스를 냅니다. 예제를 보면서 확인을 해보죠.

#generator.py
# -*- coding: utf-8 -*-
from __future__ import division
import os
import psutil
import random
import time

names = ['최용호', '지길정', '진영욱', '김세훈', '오세훈', '김민우']
majors = ['컴퓨터 공학', '국문학', '영문학', '수학', '정치']

process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / 1024 / 1024


def people_list(num_people):
    result = []
    for i in xrange(num_people):
        person = {
            'id': i,
            'name': random.choice(names),
            'major': random.choice(majors)
        }
        result.append(person)
    return result


def people_generator(num_people):
    for i in xrange(num_people):
        person = {
            'id': i,
            'name': random.choice(names),
            'major': random.choice(majors)
        }
        yield person

t1 = time.clock()
people = people_list(1000000)  #1 people_list를 호출
t2 = time.clock()
mem_after = process.memory_info().rss / 1024 / 1024
total_time = t2 - t1

print '시작 전 메모리 사용량: {} MB'.format(mem_before)
print '종료 후 메모리 사용량: {} MB'.format(mem_after)
print '총 소요된 시간: {:.6f} 초'.format(total_time)
$ python generator.py
시작 전 메모리 사용량: 8.45703125 MB
종료 후 메모리 사용량: 311.4765625 MB
총 소요된 시간: 2.677738 초

먼저 #1에서 people_list(1000000)를 호출하여 백만명의 학생의 정보가 들어가는 리스트를 만들어 봤습니다. 메모리 사용량이 8 MB에서 311 MB으로 늘었으며, 시간은 2.6초가 걸렸습니다. #1의 people_list(1000000) people_generator(1000000)로 변경하여 제너레이터의 퍼포먼스를 테스트하여 보겠습니다.

 

#generator.py
# -*- coding: utf-8 -*-
from __future__ import division
import os
import psutil
import random
import time

names = ['최용호', '지길정', '진영욱', '김세훈', '오세훈', '김민우']
majors = ['컴퓨터 공학', '국문학', '영문학', '수학', '정치']

process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / 1024 / 1024


def people_list(num_people):
    result = []
    for i in xrange(num_people):
        person = {
            'id': i,
            'name': random.choice(names),
            'major': random.choice(majors)
        }
        result.append(person)
    return result


def people_generator(num_people):
    for i in xrange(num_people):
        person = {
            'id': i,
            'name': random.choice(names),
            'major': random.choice(majors)
        }
        yield person

t1 = time.clock()
people = people_generator(1000000)  #1 people_generator를 호출
t2 = time.clock()
mem_after = process.memory_info().rss / 1024 / 1024
total_time = t2 - t1

print '시작 전 메모리 사용량: {} MB'.format(mem_before)
print '종료 후 메모리 사용량: {} MB'.format(mem_after)
print '총 소요된 시간: {:.6f} 초'.format(total_time)
$ python generator.py
시작 전 메모리 사용량: 8.42578125 MB
종료 후 메모리 사용량: 8.42578125 MB
총 소요된 시간: 0.000003 초

메모리 사용량의 변화는 없었고 시간은 1초도 걸리지 않았습니다. 이제 왜 제너레이터를 사용해야 하는지 아시겠죠?

 

 

함수 만들어서 사용

def my_function():
    print("Hello")

 

import mod1
mod1.my_function() Hello from a function