이 노트북은 제이크 반더플라스(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))
<pre><generator object at 0x103274990></pre>
제너레이터 표현식을 출력하면 내용이 출력되지 않습니다. 제너레이터 표현식의 내용을 출력하는 방법은 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
이것이 전부입니다! 계산이 더 효율적인 에라토스테네스의 체의 구현은 아니지만 제너레이터 함수 문법이 복잡한 시퀀스를 만드는데 얼마나 편리한지 보여줍니다.