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