7.4 반복자와 생성기
이 절에서는 순회 가능한 객체의 특징과 원리를 알아본다. 이 절의 내용은 약간은 이해하기 어려울 수도 있다. 여러 번 읽어봐도 이해가 되지 않는다면 무리해서 정신력을 소모하기보다 다음 장으로 건너뛰었다가 나중에 다시 봐도 된다.
7.4.1 반복자
for 문은 시퀀스뿐 아니라 집합과 사전 등 순서가 없는 컬렉션에서도 동작한다. for 문은 반복자(iterator)를 제공하는 데이터라면 모두 순회할 수 있다. 반복자는 다음에 무엇을 출력할 차례인지를 기억하여 데이터를 순서대로 꺼낼 수 있도록 돕는 인터페이스다.
반복자를 사용하려면 iter()
함수와 next()
함수가 필요하다. iter()
함수는 전달된 데이터의 반복자를 꺼내 반환한다. next()
함수는 반복자를 입력받아 그 반복자가 다음에 출력해야 할 요소를 반환한다. iter()
함수로 반복자를 구하고 그 반복자를 next()
함수에 전달하여 요소를 차례대로 꺼낼 수 있다. 데이터의 내부 구조와 동작은 몰라도 된다.
코드 7-42 반복자로 리스트 순회하기
>>> it = iter([1, 2, 3]) # [1, 2, 3]의 반복자 구하기
>>> next(it) # 반복자의 다음 요소 구하기
1
>>> next(it) # 반복자의 다음 요소 구하기
2
>>> next(it) # 반복자의 다음 요소 구하기
3
>>> next(it) # 더 구할 요소가 없으면 오류가 발생한다
StopIteration
개념 정리
- 반복 가능한 데이터:
iter()
함수로 반복자를 구할 수 있는 데이터- 반복자:
next()
함수로 값을 하나씩 꺼낼 수 있는 데이터iter()
함수: 반복 가능한 데이터를 입력받아 반복자를 반환하는 함수next()
함수: 반복자를 입력받아 다음 출력값을 반환하는 함수
iter()
함수와 next()
함수를 활용하면 for 문을 사용하지 않고도 컬렉션을 순회할 수 있다. 원한다면 for 문을 흉내내는 함수를 정의할 수도 있을 것이다. 반복자를 직접 활용할 일이 많지는 않지만, 개념을 알아 두면 파이썬 프로그래밍에 도움이 된다.
7.4.2 생성기
반복자는 흔히 컬렉션을 순회하는 데 쓰이지만, 반복자가 컬렉션만을 위한 것은 아니다. 반복자는 next()
함수에 의해 값을 하나씩 내어놓기만 할 뿐이다. 내어 놓는 값의 원천이 컬렉션이든 아니든 상관없다.
정해둔 순서대로 값을 생성하는 함수를 정의하면, 이 함수를 이용해 값을 하나씩 꺼내는 반복자를 만들 수 있다. 이런 식으로 값을 생성해 내는 반복자를 생성기(generator)라고 한다.
yield 문
함수로 생성기를 만들기 위해서는 yield 문이 필요하다. yield 문은 return 문처럼 함수가 값을 반환하고 정지하도록 하는데, 그 함수를 나중에 다시 실행하면 정지했던 위치부터 다시 실행되도록 한다. yield 문이 포함된 함수는 일반 함수와 달리, 호출했을 때 생성기를 반환한다. 다음 예제는 next()
함수에 의해 값 'a'
, 'b'
, 'c'
를 차례대로 내어 놓는 생성기를 만든 것이다.
코드 7-43 생성기 만들기
>>> def abc(): # ❶ 생성기를 반환하는 함수 정의하기
... "a, b, c를 출력하는 생성기를 반환한다."
... yield 'a'
... yield 'b'
... yield 'c'
...
>>> abc() # ❷ 생성기 만들기
<generator object abc at 0x7fdcd41583b8>
❶ 함수 abc()
를 정의했는데, 이 함수 자체는 생성기가 아니라 그냥 함수다. 생성기는 yield 문이 실행되는 함수를 실행할 때마다 새로 만들어진다. ❷와 같이 abc()
를 실행했을 때 그 결과로 생성기 <generator object abc at 0x7fdcd41583b8>
가 만들어진 것을 볼 수 있다. 생성기는 반복자의 일종이므로, 이렇게 만든 생성기를 next()
함수에 전달해 값을 하나씩 꺼낼 수 있다.
코드 7-44 next()
함수로 생성기에서 값 꺼내기
>>> abc_generator = abc() # ❶ 생성기 만들기
>>> next(abc_generator) # ❷ 생성기의 다음 값 꺼내기
'a'
>>> next(abc_generator)
'b'
>>> next(abc_generator)
'c'
>>> next(abc_generator) # ❸ 더 구할 요소가 없으면 오류가 발생한다
StopIteration
❶ 함수 abc()
를 호출해 생성기를 만들어 abc_generator
변수에 대입했다. ❷ 만든 생성기를 next()
함수에 전달해 값을 순서대로 꺼낼 수 있다. 이 때 생성되는 값은 생성기를 만드는 데 사용한 함수 abc()
에서 yield 문에 정의된 값이다. yield 문이 세 번에 걸쳐 ‘a’, ‘b’, ‘c’를 내도록 정의되어 있기 때문에, 이 함수를 통해 만들어진 생성기는 ‘a’, ‘b’, ‘c’ 세 값을 내어 놓는다. ❸ 그 후 next()
함수로 한 번 더 값을 꺼내도록 지시하면 더 꺼낼 값이 없으므로 오류가 발생한다.
yield 문을 포함한 함수는 일반적인 함수와 실행 방식이 다르다. 이 함수는 호출되어도 본문을 실행하지 않고 생성기를 반환할 뿐이며, 본문은 생성기가 next()
함수에 전달되었을 때 비로소 실행된다. 본문에서 yield 문이 등장하면, 생성기는 yield 문에 정의된 값을 반환한 후 실행을 멈춘다. next()
함수에 의해 생성기가 다시 실행되면 멈춘 위치에서부터 실행이 이어진다. 함수 중간중간에 print()
함수로 진행 상황을 출력해보자.
코드 7-45 생성기 함수의 본문은 next()
함수에 의해 실행된다
>>> def one_to_three():
... """1, 2, 3을 반환하는 생성기를 반환한다."""
... print('생성기가 1을 내어 놓습니다.')
... yield 1
... print('생성기가 2를 내어 놓습니다.')
... yield 2
... print('생성기가 3을 내어 놓습니다.')
... yield 3
...
>>> one_to_three_generator = one_to_three()
>>> next(one_to_three_generator)
생성기가 1을 내어 놓습니다.
1
>>> next(one_to_three_generator)
생성기가 2를 내어 놓습니다.
2
>>> next(one_to_three_generator)
생성기가 3을 내어 놓습니다.
3
개념 정리
- 생성기는 반복자의 한 종류다.
- 생성기는 yield 문이 포함된 함수를 실행하여 만들 수 있다.
- yield 문이 포함된 함수를 실행하면 생성기가 반환된다. 생성기를
next()
함수에 전달해 실행시키면 함수의 본문이 실행된다.- yield 문은 값을 내어준 후 생성기의 실행을 일시정지한다.
next()
함수가 실행되면 정지했던 위치에서부터 다시 실행이 이어진다.
생성기를 어디에 활용할까?
생성기는 원본이 되는 데이터가 없더라도 순회할 수 있다. 순회하는 시점에 데이터를 생성해 낸다. 컴퓨터의 메모리는 한정되어 있으므로 순회해야 할 데이터를 미리 정의해 두는 것이 불가능할 때도 있다. 예를 들어, 1부터 무한대까지의 모든 자연수를 담은 컬렉션은 컴퓨터에 미리 만들어 저장할 수 없다. 하지만 생성기로 필요한 값을 하나씩 꺼내도록 하면 무한대의 컬렉션을 흉내낼 수 있다.
코드 7-46 1부터 무한대 범위의 자연수를 출력하는 생성기
>>> def one_to_infinite():
... """1 - 무한대의 자연수를 순서대로 내는 생성기를 반환한다."""
... n = 1 # n은 1에서 시작한다
... while True: # ❶ 무한 반복
... yield n # ❷ 실행을 일시정지하고 n을 반환한다
... n += 1 # n에 1을 더한다
...
>>> natural_numbers = one_to_infinite() # ❸ 생성기를 만들어
>>> next(natural_numbers) # 수를 무한히 꺼낼 수 있다
1
>>> next(natural_numbers)
2
one_to_infinite()
함수는 ❶ 무한 반복된다. 하지만 ❷ yield 문이 값을 하나 반환하고 실행을 일시정지하므로 무한 반복이 있더라도 함수가 그 때 그 때 종료된다. ❸ 이 함수를 실행하여 생성기를 만들면 next()
함수로 1부터 무한대까지의 수를 원하는 만큼 꺼낼 수 있다.
생성기는 각 요소를 구하는 비용이 클 때도 활용하면 좋다. 요소 하나를 구하는 데 1초가 걸린다면, 1천 개짜리 컬렉션을 미리 만들기 위해서는 1천 초가 필요하다. 하지만 생성기로 각 요소가 필요할 때만 하나씩 구한다면 요소를 꺼낼 때 1초씩 걸리긴 하겠지만 미리 시간을 들여 요소를 준비하지는 않아도 된다.
생성기는 반복자이므로, 반복자와 마찬가지로 for 문으로 순회하거나 리스트·튜플로 변환할 수 있다.
코드 7-47 생성기를 리스트와 튜플로 변환하기
>>> def countdown(start, end):
... """start(포함)부터 end(비포함)까지 거꾸로 세는 생성기를 반환한다."""
... n = start # n은 start에서 시작한다
... while end < n: # n이 end에 도달하지 않은 동안 반복한다
... yield n # 실행을 일시정지하고 n을 반환한다
... n -= 1
...
>>> list(countdown(10, 0)) # 생성기를 리스트로 변환하기
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> tuple(countdown(100, 95)) # 생성기를 튜플로 변환하기
(100, 99, 98, 97, 96)
단, 무한한 범위의 생성기를 리스트·튜플로 변환할 수는 없다.
개념 정리
- 생성기는 일반적인 반복자처럼 순회할 수 있다.
- 생성기를 이용해 규모가 매우 큰 컬렉션을 흉내낼 수 있다.
- 요소를 생성하는 비용이 큰 컬렉션은 모든 요소를 미리 만들기보다, 생성기를 이용해 그 때 그 때 필요한 요소만 생성하는 편이 좋을 수도 있다.
연습문제
연습문제 7-14 무한 난수 생성기
난수(random number)란 예측할 수 없는 임의의 수를 말한다. 현실에서는 주사위를 던져 난수를 구할 수 있다. 파이썬에서는 random
모듈(11장)의 random.randint()
함수를 이용해 매개변수로 지정한 범위 사이의의 난수를 구할 수 있다. 다음은 random.randint()
함수를 사용하는 예다.
>>> import random # random 모듈 임포트
>>> random.randint(0, 63) # 0 이상 63 이하의 임의의 수
24
>>> random.randint(0, 63)
62
>>> random.randint(0, 63)
0
>>> [random.randint(0, 63) for _ in range(5)] # 난수 5개의 리스트
[39, 38, 43, 46, 29]
random.randint()
함수를 이용해 무한한 개수의 난수를 꺼낼 수 있는 무한 난수 생성기를 만들어 보아라.
7.4.3 생성기 식
생성기 식(generator expression)은 생성기를 표현하는 식이다. 생성기 식은 원본 반복 가능 데이터를 가공하여 데이터를 생성한다. 생성기 식은 앞에서 배운 리스트 조건제시법과 유사하니, 리스트 조건제시법과 비교하며 알아보자.
리스트 조건제시법을 사용하기 곤란할 때
코드 7-48 리스트 조건제시법으로 표현한 세제곱수 리스트
>>> [e ** 3 for e in range(10)]
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
위 코드는 0부터 10개의 세제곱수를 담은 리스트를 리스트 조건제시법으로 표현한 것이다. 리스트 조건제시법을 평가하면 원본 컬렉션을 조건에 따라 계산하여 새 리스트가 만들어진다. 새 리스트가 즉시 만들어진다는 점이 중요한데, 변형할 요소의 양이 적을 때는 이 점이 문제가 되지 않는다. 하지만 세제곱수를 0부터 10억 개까지 담은 리스트를 만든다면 어떻게 될까?
코드 7-49 리스트 조건제시법으로 세제곱수를 10억 개 만들기
>>> [e ** 3 for e in range(1000000000)] # 오랜 시간이 걸린다
위 코드를 대화식 셸에서 실행하면, 계산하는 데 오랜 시간이 걸려서 대화식 셸이 멈춘 것처럼 보일 것이다. (컨트롤 + c 키를 눌러 명령을 취소하자.) 메모리가 부족해 오류가 발생할 수도 있다. 이처럼 리스트의 크기가 클 때는 리스트 조건제시법을 사용하기 어렵다.
생성기 식으로 대체하기
다음은 생성기 식의 양식이다. 리스트 조건제시법과 거의 똑같다. 대괄호를 소괄호로 바꾸었을 뿐이다.
(연산 for 변수 in 컬렉션 if 조건)
생성기 식이 하는 일도 리스트 조건제시법과 거의 똑같다. 컬렉션의 각 요소에 연산을 적용한다. 그러나 생성기 식은 리스트를 바로 생성하는 것이 아니라 생성기를 만들어 낸다. 그러므로 즉시 모든 요소에 연산을 수행하는 것이 아니라, 각 요소를 꺼내야 하는 시점에만 연산을 수행하도록 할 수 있다. 코드 7-49를 생성기 식으로 수정하여 실행해 보자.
코드 7-50 세제곱수를 10억 개 만들어내는 생성기
>>> (e ** 3 for e in range(1000000000)) # 대괄호가 아닌 괄호 사용
<generator object <genexpr> at 0x7fdcd41584c0>
레인지를 바로 리스트로 변환하는 작업이 일어나지 않고, <generator object <genexpr> at 0x7fdcd41584c0>
와 같은 생성기 객체가 만들어졌다. 이 생성기를 변수에 대입해 next()
함수에 전달하면 세제곱수를 하나씩 구할 수 있다.
코드 7-51 생성기 식으로 만든 생성기를 next()
함수로 실행하기
>>> cube_generator = (e ** 3 for e in range(1000000000))
>>> next(cube_generator)
0
>>> next(cube_generator)
1
>>> next(cube_generator)
8
생성기 식을 이용하면 원본 컬렉션을 다른 것으로 변환하는 종류의 생성기를 간편하게 만들 수 있다. 각 요소를 구하는 비용이 크거나 원본 컬렉션의 크기가 방대할 때는 리스트 조건제시법 대신 생성기 식을 사용하는 것이 유리하다.
연습문제
연습문제 7-15 주문을 즉시 처리하기
사용자의 음료 주문을 받아 제조를 지시하는 프로그램을 만들었다. input_orders()
함수는 사용자로부터 n 개의 주문을 입력받아 리스트로 반환한다. 이 함수를 이용해 음료를 세 개 입력받고 제조를 지시하도록 했다.
def input_orders(n):
"""n개의 음료를 주문받아 리스트로 반환한다."""
return [input() for _ in range(n)]
# 음료 주문 세 개를 입력받아 각 음료마다 제조 지시한다
for drink in input_orders(3):
print(drink, '만들어 주세요!')
이 프로그램을 실행하면, 음료 세 개를 먼저 입력받은 뒤 이어서 제조 지시를 세 번 한다.
아메리카노
카페 라테
딸기 주스
아메리카노 만들어 주세요!
카페 라테 만들어 주세요!
딸기 주스 만들어 주세요!
이 프로그램의 input_names()
함수에서 사용된 리스트 조건제시법을 생성기 식으로 수정하고 프로그램을 실행해 보아라. 실행 결과가 어떻게 달라지는지 확인하고, 왜 그런지 설명해 보아라.
댓글 남기기