generator는 generator iterator라고 불리는 특별한 종류의 이터레이터를 반환하는 함수나 표현식이에요.
Generator-iterator는 게으른 평가를 따르는데, values를 메모리에 저장해 두지 않고 필요할 때 값들을 _생성_해요.
제너레이터 함수는 다른 함수와 똑같이 생겼지만, 하나 이상의 yield 표현식을 포함해요.
yield를 만나면 코드 실행이 잠시 멈추고, 현재 실행 상태(모든 지역 변수와 try 문을 포함한)를 저장해요.
제너레이터가 다시 실행되면, 중단된 지점의 상태를 그대로 이어받아요. 호출할 때마다 초기화되는 일반 함수와는 다르죠.
제너레이터는 다른 반복문이나 재귀 함수와 비슷하게 만들지만, yield 표현식이 필요해요. 이건 조금 뒤에 자세히 살펴볼게요.
예를 들어 주어진 숫자 목록에서 각 숫자의 _제곱_을 반환하는 함수를 생각해 봐요. 지금처럼 작성하면, 값을 하나라도 반환하기 전에 모든 입력을 다 처리해야 해요:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
이 함수를 다음과 같이 제너레이터로 바꿀 수 있어요:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
이렇게 하는 이유는, 모든 값을 한꺼번에 만들 필요가 없을 때 제너레이터를 쓰기 때문이에요. 지금 작업 중인 값만 계산하니 메모리와 처리 성능을 아낄 수 있죠.
제너레이터는 Python에서 대부분의 iterables 대신 사용할 수 있어요.
iterable이나 iterator를 인자로 요구하는 _함수_나 _객체_도 마찬가지죠.
squares_generator() 제너레이터를 사용하려면 이렇게 해요:
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
generator 안의 값은 next() 함수로도 꺼내거나 접근할 수 있어요.
next()는 제너레이터-이터레이터 객체의 __next__() 메서드를 호출해서 코드를 yield 표현식까지 "진행"시키거나 평가하고, 그러면 값 하나를 "yield"하거나 반환해요:
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
generator-iterator를 끝까지 다 써서 더 반환할 값이 없으면, StopIteration 오류를 던져요.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
제너레이터-이터레이터는 이터레이터의 특별한 부분집합이에요.
Iterators는 _이터러블_을 반복할 수 있게 해 주는 메커니즘, 즉 프로토콜이에요.
제너레이터-이터레이터와 일반적인 Python iterables가 반환하는 이터레이터는 아주 비슷하게 동작하지만, 몇 가지 중요한 차이점이 있어요:
indexes가 없어요.
덧셈이나 뺄셈으로 이전 값이나 이후 값을 참조할 수 없고, 대괄호([]) 표기법이나 슬라이싱도 쓸 수 없어요.len() 함수와 함께 쓸 수 없어요.generator-iterator에서 값을 전부 모을 때는 조심해요!yield 표현식은 return 표현식과 아주 비슷해요.
return 표현식과는 달리, yield는 _특정 지점_에서 호출한 쪽에 값을 넘겨주고, 그 값들이 다시 요청될 때까지 추가 값의 평가와 반환을 멈춰요.
yield가 평가되면, 감싸고 있는 함수의 실행을 잠시 멈추고 그 시점의 함수 값들을 반환해요.
그러면 함수는 스코프에 그대로 남아 있고, __next__()가 호출되면 다시 yield를 만날 때까지 실행을 이어가요.
함수 밖에서는 yield 표현식을 사용할 수 없어요.
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
제너레이터는 여러 곳에서 유용하게 쓰여요.
값이 아주 많을 수도 있는 컬렉션을 다룰 때는, 그 값들을 전부 메모리에 올리고 싶지 않을 수 있어요. 제너레이터를 쓰면 큰 데이터를 조각조각 처리하면서 메모리를 아끼고 성능을 높일 수 있죠.
처리나 계산이 _복잡_하거나 _비용이 많이 들_거나 _무한_할 때도 제너레이터가 아주 유용해요:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
이제 infinite_sequence 객체에 __next__()를 호출할 때마다 이전 숫자 + 1을 반환해요.
Conda Airlines는 하루에 10,000편이 넘는 항공편을 운항하는, 프로그래밍 세계에서 가장 큰 항공사예요!
지금은 모든 좌석을 승객에게 일일이 손으로 배정하고 있는데, 이 작업은 자동화가 필요해요.
그래서 승객 좌석 배정을 자동화할 소프트웨어를 만들어 달라고 _여러분_에게 요청했어요. 이 소프트웨어는 메모리 효율이 좋고 성능도 뛰어나야 해요.
Conda는 비행기의 좌석 문자를 만들고 싶어 해요.
비행기는 좌석이 여러 행으로 이루어져 있어요.
각 행에는 좌석이 네 개 있어요.
각 행의 좌석 이름은 항상 A, B, C, D예요.
행의 첫 번째 좌석은 A, 두 번째 좌석은 B, 이런 식으로 이름이 붙어요.
D까지 가면 다시 A부터 시작해요.
생성할 좌석 문자의 개수를 담은 int를 인자로 받는 generate_seat_letters(<number>) 함수를 구현해요.
그러면 함수는 좌석 문자를 담은 _이터러블_을 반환해야 해요.
>>> letters = generate_seat_letters(4)
>>> next(letters)
"A"
>>> next(letters)
"B"
Conda Airlines는 비행기에 필요한 만큼의 좌석을 만들어 내는 시스템을 원해요.
각 비행기는 행마다 좌석이 네 개 있어요.
행은 1부터 시작해서 숫자가 하나씩 커지는 방식으로 나타내요.
좌석은 1A, 1B, 1C, 1D, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, ... 순서로 정렬되어야 해요.
예시를 볼까요:
| x | 1 | 2 |
|---|---|---|
| 행 | 5 | 21 |
| 좌석 문자 | A | D |
| 결과 | 5A | 21D |
많은 항공사는 승객들의 미신 때문에 13번 _행_을 두지 않아요. Conda Airlines도 이 관례를 따르니, 13번 _행_의 좌석은 만들지 않도록 해요.
생성할 좌석의 개수를 담은 int를 인자로 받는 generate_seats(<number>) 함수를 구현해요.
그러면 함수는 만들어진 좌석을 담은 _이터러블_을 반환해야 해요.
>>> seats = generate_seats(10)
>>> next(seats)
"1A"
>>> next(seats)
"1B"
이제 좌석을 생성하는 함수가 있으니, 이 함수로 승객에게 좌석을 배정할 수 있어요.
승객 이름이 담긴 list를 인자로 받는 assign_seats(<passengers>) 함수를 구현해요.
그러면 함수는 passenger를 _키_로, seat_number를 _값_으로 갖는 _딕셔너리_를 반환해야 해요.
>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob']
>>> assign_seats(passengers)
{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'}
Conda Airlines는 티켓마다 고유한 코드를 부여하고 싶어 해요. 규모가 큰 항공사인 만큼 운항하는 항공편도 아주 많아요. 그래서 같은 좌석 번호를 가진 항공편이 여러 개 있을 수 있어요. 그래서 _12_자 길이의 고유한 티켓 문자열을 만들어 내는 시스템을 원해요.
이 코드는 assigned_seat로 시작하고, 그 뒤에 flight_id가 와요.
나머지 부분은 0s로 채워요.
seat_numbers가 담긴 list와 항공편 번호가 담긴 string을 인자로 받는 generate_codes(<seat_numbers>, <flight_id>) 함수를 구현해요.
그러면 함수는 ticket_number를 만들어 내는 generator를 반환해야 해요.
>>> seat_numbers = ['1A', '17D']
>>> flight_id = 'CO1234'
>>> ticket_ids = generate_codes(seat_numbers, flight_id)
>>> next(ticket_ids)
'1ACO12340000'
>>> next(ticket_ids)
'17DCO1234000'
Exercism에 가입하고 Python 트랙을 개념 17개연습 문제 146개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.