이 노트북은 제이크 반더플라스(Jake VanderPlas)의 A Whirlwind Tour of Python(OReilly Media, 2016)를 기반으로 만들어졌습니다. 이 내용은 CC0 라이센스를 따릅니다. 전체 노트북의 목록은 https://github.com/rickiepark/WhirlwindTourOfPython 에서 볼 수 있습니다.
여기에서 제너레이터 표현식과 제너레이터 함수를 포함하여 파이썬 제너레이터를 자세히 배워 보겠습니다.
리스트 내포와 제너레이터 표현식의 차이는 이따금 헷갈립니다. 이 둘의 차이를 간단히 살펴 보겠습니다:
다음은 대표적인 리스트 내포입니다:
[n ** 2 for n in range(12)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
반면 다음은 대표적인 제너레이터 표현식입니다:
(n ** 2 for n in range(12))
<generator object <genexpr> at 0x7f15e4b23200>
제너레이터 표현식을 출력하면 내용이 출력되지 않습니다. 제너레이터 표현식의 내용을 출력하는 방법은 list
생성 함수에 전달하는 것입니다:
G = (n ** 2 for n in range(12))
list(G)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
리스트를 만들 때 실제 일련의 어떤 값들을 만드므로 연관되어 일정량의 메모리가 소모됩니다. 제너레이터를 만들 때는 일련의 값들을 만들지 않고 이런 값들을 만드는 방법을 만듭니다. 둘 다 같은 반복자 인터페이스에 사용할 수 있습니다:
L = [n ** 2 for n in range(12)]
for val in L:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121
G = (n ** 2 for n in range(12))
for val in G:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121
차이는 제너레이터 표현식은 필요할 때까지 실제로 값을 계산하지 않는다는 점입니다. 메모리를 효율적으로 사용할 수 있을 뿐만 아니라 계산 비용도 절감할 수 있습니다! 리스트의 크기는 가용 메모리 범위로 제한되지만 제너레이터의 크기는 제한이 없다는 뜻이 됩니다!
무한 제너레이터 표현식의 한 예는 itertools
에 정의된 count
반복자를 사용하여 만들 수 있습니다:
from itertools import count
count()
count(0)
for i in count():
print(i, end=' ')
if i >= 10: break
0 1 2 3 4 5 6 7 8 9 10
count
반복자는 멈출 때까지 영원히 카운팅할 것입니다. 영원히 실행되는 제너레이터를 만드는 것도 쉽습니다:
factors = [2, 3, 5, 7]
G = (i for i in count() if all(i % n > 0 for n in factors))
for val in G:
print(val, end=' ')
if val > 40: break
1 11 13 17 19 23 29 31 37 41
적절히 factors
리스트를 확장히키면 에라토스테네스의 체 알고리즘을 사용하여 소수 제너레이터를 구성할 수 있습니다. 조금 후에 이에 대해 더 자세히 알아 보겠습니다.
이것이 제너레이터 표현식의 장점 중 하나입니다. 리스트를 사용하면 다음과 같이 쉽게 할 수 있습니다:
L = [n ** 2 for n in range(12)]
for val in L:
print(val, end=' ')
print()
for val in L:
print(val, end=' ')
0 1 4 9 16 25 36 49 64 81 100 121 0 1 4 9 16 25 36 49 64 81 100 121
하지만 제너레이터 표현식은 한 번 반복하면 사라집니다:
G = (n ** 2 for n in range(12))
list(G)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
list(G)
[]
이런 기능 덕택에 반복이 중지되고 다시 시작될 수 있기 때문에 매우 유용합니다:
G = (n**2 for n in range(12))
for n in G:
print(n, end=' ')
if n > 30: break
print("\n그 사이에 무언가 다른 일을 합니다")
for n in G:
print(n, end=' ')
0 1 4 9 16 25 36 그 사이에 무언가 다른 일을 합니다 49 64 81 100 121
디스크에 있는 데이터 파일들을 다룰 때 매우 유용합니다. 배치 처리를 할 때 제너레이터가 처리한 파일들을 기억할 수 있습니다.
yield
를 사용¶이전 절에서 리스트 내포는 비교적 간단한 리스트를 만드는데 좋고 보통의 for
루프는 아주 복잡한 상황에 더 잘 맞습니다.
제너레이터 표현도 동일합니다. yield
문을 사용하는 제너레이터 함수를 사용하여 더 복잡한 제너레이터를 만들 수 있습니다.
동일한 리스트를 만드는 두 가지 방법이 다음에 나타나 있습니다:
L1 = [n ** 2 for n in range(12)]
L2 = []
for n in range(12):
L2.append(n ** 2)
print(L1)
print(L2)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
비슷하게 동일한 제너레이터를 만드는 두 가지 방법이 있습니다:
G1 = (n ** 2 for n in range(12))
def gen():
for n in range(12):
yield n ** 2
G2 = gen()
print(*G1)
print(*G2)
0 1 4 9 16 25 36 49 64 81 100 121 0 1 4 9 16 25 36 49 64 81 100 121
제너레이터 함수는 한 번 값을 리턴하는 return
대신 (무한도 가능한) 시퀀스를 반환하는 yield
를 사용한 함수입니다.
제너레이터 표현식과 마찬가지로 제너레이터의 상태는 부분 반복 간에 유지됩니다. 새로운 제너레이터를 얻고 싶으면 간단히 함수를 다시 호출하면 됩니다.
다음 제가 제일 좋아하는 제너레이터 함수의 예를 보겠습니다. 무한한 소수를 생성하는 함수입니다. 전통적인 알고리즘은 다음과 같이 작동하는 에라토스테네스의 체입니다:
# 후보 리스트를 생성합니다
L = [n for n in range(2, 40)]
print(L)
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
# 첫 번째 값의 모든 배수를 제거합니다
L = [n for n in L if n == L[0] or n % L[0] > 0]
print(L)
[2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39]
# 두 번째 값의 모든 배수를 제거합니다
L = [n for n in L if n == L[1] or n % L[1] > 0]
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 25, 29, 31, 35, 37]
# 세 번째 값의 모든 배수를 제거합니다
L = [n for n in L if n == L[2] or n % L[2] > 0]
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
아주 충분히 큰 리스트에서 충분한 시간동안 이를 반복하면 원하는 만큼의 소수를 생성할 수 있습니다.
이 로직을 제러레이터 함수에 캡슐화해 보죠:
def gen_primes(N):
"""N까지의 소수를 생성합니다"""
primes = set()
for n in range(2, N):
if all(n % p > 0 for p in primes):
primes.add(n)
yield n
print(*gen_primes(100))
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
이것이 전부입니다! 계산이 더 효율적인 에라토스테네스의 체의 구현은 아니지만 제너레이터 함수 문법이 복잡한 시퀀스를 만드는데 얼마나 편리한지 보여줍니다.