Python 강좌 : 제 42강 - 생성자
생성자(Generator)
생성자(Generator, 제너레이터)란 반복자(Iterator)를 간편하게 만들어주는 함수입니다.
yield 키워드를 활용해 반복자 형태를 구현할 수 있습니다.
함수 내부에 yield가 존재하면, 그 함수는 생성자 함수(Generator Function)가 되며, 호출하면 함수 본문을 바로 실행하지 않고 생성자 객체(Generator Object)를 반환합니다.
생성자 객체는 __iter__()와 __next__() 메서드를 가진 반복자입니다.
일반적인 함수는 스코프(Scope)내의 코드를 모두 실행시킨 후, 소멸합니다.
하지만, 생성자 함수는 스코프 내에서 일시 중지해가며 실행시킬 수 있습니다.
즉, 함수가 실행되고 있는 도중에 중지하고, 지역 변수의 상태를 유지한 채 나중에 이어서 실행할 수 있습니다.
제 41강에서는 반복자를 만들기 위해 클래스에 __iter__()와 __next__()를 정의하고, 진행 상태를 self.current 같은 속성에 직접 저장했습니다.
생성자를 사용하면 이 작업을 Python이 대신합니다. 지역 변수가 곧 상태가 되고, yield가 곧 “다음 값을 반환하고 멈추기”가 되며, 함수가 끝나면 StopIteration이 자동으로 발생합니다.
그러므로 생성자는 값을 하나씩 만들어 내는 반복자를 짧고 읽기 쉬운 코드로 작성할 때 사용합니다. 대용량 파일을 한 줄씩 처리하거나, 끝이 없는 수열을 만들거나, 여러 단계의 데이터 처리를 연결할 때 특히 유용합니다.
이번 강좌에서는 생성자가 언제 실행되고 어디서 멈추는지를 단계별로 따라가고, yield from, 생성자 표현식, 그리고 생성자에 값을 보내는 send(), throw(), close()를 다룹니다.
- Tip : 클래스의
__init__()메서드를 가리키는생성자(Constructor)와는 다른 개념입니다. 혼동을 피하기 위해제너레이터라고 부르기도 합니다.
생성자(Generator) 사용하기
생성자를 이해하는 핵심은 “함수를 호출하는 것”과 “본문이 실행되는 것”이 분리되어 있다는 점입니다.
다음 예제는 print()를 곳곳에 넣어, 본문의 어느 부분이 어느 next() 호출에서 실행되는지 보여 줍니다.
def generator():
data = [0]
print("First")
data.append(1)
yield data
print("Second")
data.append(2)
yield data
print("Third")
gen = generator()
print(next(gen))
print("———")
print(next(gen))
print("———")
print(next(gen, "END"))
print("———")
print(next(gen, "END"))
print("———")- 결과
- First
[0, 1]
———
Second
[0, 1, 2]
———
Third
END
———
END
———
생성자(Generator)는 yield 키워드를 통해 구현할 수 있습니다.
generator()를 호출하면 본문이 실행되지 않고 생성자 객체가 반환됩니다. 본문은 next() 함수를 호출할 때 비로소 실행됩니다.
next() 함수를 실행시킬 때, yield 키워드 구문에서 값을 반환하고 일시 중지하게 됩니다.
다음 next() 호출 때는 함수를 처음부터 다시 실행하는 것이 아니라, 중지했던 yield 구문 다음부터 이어서 실행합니다.
함수 내부의 변수나 멤버는 유지되며, 변경이 가능합니다.
yield 키워드가 더 이상 남아있지 않을 때에는 남은 구문을 모두 실행한 뒤, StopIteration 예외를 발생시킵니다.
하지만, next(생성자, 기본값)처럼 기본값을 전달하면 StopIteration 예외 대신 기본값을 반환합니다.
StopIteration 예외 발생 이후에도 호출한다면, 더 이상 실행할 구문이 없어 END 값만 반환합니다.
결과를 한 줄씩 따라가면 다음과 같습니다.
gen = generator(): 아무것도 출력되지 않습니다. 생성자 객체만 만들어지고, 실행 위치는 본문의 맨 앞에 있습니다.- 첫 번째
next(gen): 본문의 처음부터 실행되어First를 출력하고,data에1을 추가한 뒤 첫 번째yield data에서[0, 1]을 반환하며 멈춥니다. - 두 번째
next(gen): 첫 번째yield바로 다음 줄부터 실행되어Second를 출력합니다. 지역 변수data는 이전 값[0, 1]을 그대로 가지고 있으므로2를 추가해[0, 1, 2]를 반환하며 멈춥니다. - 세 번째
next(gen, "END"):Third를 출력하고 함수 끝에 도달합니다. 반환할yield가 없으므로StopIteration이 발생하고, 기본값END가 반환됩니다. - 네 번째
next(gen, "END"): 이미 끝난 생성자이므로 본문은 전혀 실행되지 않고 곧바로END가 반환됩니다.
- Tip : 두
yield가 반환한 값은 같은 리스트 객체입니다. 첫 번째로 받은 리스트를 변수에 저장해 두었다면, 두 번째next()이후에는 그 변수도[0, 1, 2]로 보입니다.
import inspect
def gen():
print("시작")
x = 1
yield x
print("재개")
x += 1
yield x
print("끝")
g = gen()
print(inspect.getgeneratorstate(g))
print(next(g))
print(inspect.getgeneratorstate(g), dict(g.gi_frame.f_locals))
print(next(g))
print(inspect.getgeneratorstate(g), dict(g.gi_frame.f_locals))
print(next(g, "END"))
print(inspect.getgeneratorstate(g), g.gi_frame)- 결과
- GEN_CREATED
시작
1
GEN_SUSPENDED {‘x’: 1}
재개
2
GEN_SUSPENDED {‘x’: 2}
끝
END
GEN_CLOSED None
생성자가 멈춰 있는 동안 무엇이 저장되어 있는지 직접 확인하는 예제입니다.
inspect.getgeneratorstate()는 생성자의 현재 상태를 반환하며, gi_frame은 생성자가 실행되는 프레임(frame)입니다. 프레임에는 지역 변수와 다음에 실행할 위치가 저장되어 있습니다.
| 상태 | 의미 |
|---|---|
| GEN_CREATED | 만들어졌지만 아직 한 번도 실행되지 않음 |
| GEN_RUNNING | 지금 실행 중 |
| GEN_SUSPENDED | yield에서 일시 정지 |
| GEN_CLOSED | 실행이 끝났거나 닫힘 |
생성 직후에는 GEN_CREATED 상태이며, 첫 번째 next()가 시작을 출력하고 yield x에서 1을 반환한 뒤 GEN_SUSPENDED 상태가 됩니다. 이때 프레임에는 지역 변수 x가 1로 보관되어 있습니다.
두 번째 next()는 재개를 출력하고, 보관된 x에 1을 더해 2를 반환하며 다시 멈춥니다.
세 번째 호출에서 끝을 출력하고 함수가 끝나면, 상태는 GEN_CLOSED가 되고 더 이상 필요 없는 프레임은 해제되어 gi_frame이 None이 됩니다.
즉, 일반 함수의 프레임은 return과 함께 사라지지만, 생성자의 프레임은 yield에서 사라지지 않고 보관되었다가 다음 next()에서 그대로 이어서 사용됩니다.
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(3)))
g = countdown(3)
print(iter(g) is g, list(g), list(g))- 결과
- [3, 2, 1]
True [3, 2, 1] []
제 41강에서 클래스로 구현한 Countdown 반복자를 생성자로 다시 작성한 예제입니다.
__iter__(), __next__(), StopIteration을 직접 작성하지 않아도 같은 결과를 얻을 수 있으며, 진행 상태는 지역 변수 start에 자연스럽게 저장됩니다.
생성자 객체도 반복자이므로 iter(g) is g는 True이며, 한 번 끝까지 순회하면 두 번째 list(g)는 빈 리스트를 반환합니다. 다시 순회하려면 countdown(3)을 다시 호출해 새 생성자 객체를 만듭니다.
from itertools import islice
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fibonacci(), 10)))- 결과
- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
while True로 끝없이 값을 만드는 피보나치 수열 생성자입니다.
일반 함수였다면 무한 반복에 빠져 돌아오지 않겠지만, 생성자는 yield마다 멈추므로 요청한 만큼만 계산합니다. islice()로 10개를 요청했으므로 정확히 10번만 실행되고 멈춥니다.
생성자의 반환값
생성자 함수 안에서도 return을 사용할 수 있습니다. 다만 일반 함수와 달리, return의 값은 next()의 반환값이 되지 않고 종료 신호에 함께 실려 전달됩니다.
def gen():
yield 1
return "done"
it = gen()
print(next(it))
try:
next(it)
except StopIteration as e:
print(e.value)- 결과
- 1
done
생성자 함수에서 return 값을 사용하면, 생성자가 종료되면서 발생하는 StopIteration 예외의 value 속성에 해당 값이 담깁니다.
첫 번째 next(it)는 yield 1에서 1을 반환합니다. 두 번째 next(it)는 return "done"에 도달해 생성자를 끝내며, 이때 발생한 StopIteration의 value에 "done"이 저장됩니다.
for 반복문이나 list()로 순회할 때에는 이 반환값이 무시되며, yield로 생성한 값만 사용됩니다.
생성자의 반환값은 주로 아래에서 다룰 yield from으로 하위 생성자의 최종 결과를 받을 때 사용합니다.
반복 가능한(iterable) 형식 반환하기
yield는 값을 하나 내보내는 구문이므로, 리스트를 yield하면 리스트 전체가 값 하나로 취급됩니다.
리스트 안의 요소를 하나씩 내보내고 싶다면 반복문을 쓰거나 yield from을 사용합니다.
def generator():
value = [1, 2, 3]
yield value
gen = generator()
print(list(gen))- 결과
- [[1, 2, 3]]
yield 키워드를 통해 반복 가능한(iterable) 객체를 반환할 경우, 객체 자체가 하나의 값으로 반환됩니다.
list(gen)은 생성자가 내보낸 값을 모아 리스트로 만드는데, 내보낸 값이 [1, 2, 3] 하나뿐이므로 리스트 안에 리스트가 들어 있는 [[1, 2, 3]]이 됩니다.
이를 해결하기 위해 from 키워드를 추가해 요소를 하나씩 반환할 수 있습니다.
def generator():
value = [1, 2, 3]
yield from value
gen = generator()
print(list(gen))- 결과
- [1, 2, 3]
yield from iterable 형태로 값을 반환하면, 반복 가능한 객체의 요소를 하나씩 차례대로 반환합니다.
즉, for v in value: yield v와 같은 의미이며, 불필요한 반복문을 작성하지 않아도 됩니다.
yield from에서도 생성자는 요소 하나를 내보낼 때마다 멈춥니다. 그러므로 next()를 한 번 호출하면 1만 반환되고, 나머지는 다음 호출을 기다립니다.
def sub():
yield "a"
yield "b"
return 2
def main():
r = yield from sub()
print("sub returned", r)
yield r
print(list(main()))- 결과
- sub returned 2
[‘a’, ‘b’, 2]
yield from에 다른 생성자를 전달하면, 해당 생성자에게 실행을 위임(delegate)합니다.
하위 생성자의 yield 값은 그대로 바깥으로 전달되며, 하위 생성자가 return으로 반환한 값은 yield from 표현식의 결과가 됩니다.
실행 순서는 다음과 같습니다. list()가 main()에 값을 요청하면 main()은 sub()에게 위임하여 "a", "b"가 바깥으로 그대로 전달됩니다.
sub()가 return 2로 끝나면 그 값이 r에 대입되고, main()이 다시 실행되어 sub returned 2를 출력한 뒤 yield r로 2를 내보냅니다.
print()가 리스트보다 먼저 출력된 이유는, list()가 생성자를 끝까지 실행한 다음에 리스트를 반환하고 그때 비로소 바깥의 print()가 실행되기 때문입니다.
또한, send()나 throw()로 전달한 값과 예외도 하위 생성자로 그대로 전달됩니다.
def numbers():
print("numbers 시작")
for i in range(1, 4):
print(f" {i} 생성")
yield i
def squares(it):
for x in it:
print(f" {x} 제곱")
yield x * x
pipe = squares(numbers())
print("파이프라인 생성")
for v in pipe:
print(v)- 결과
- 파이프라인 생성
numbers 시작
1 생성
1 제곱
1
2 생성
2 제곱
4
3 생성
3 제곱
9
생성자의 출력을 다른 생성자의 입력으로 연결하면 파이프라인(Pipeline)을 만들 수 있습니다.
squares(numbers())를 호출해도 두 생성자 모두 아직 실행되지 않으므로, 가장 먼저 파이프라인 생성이 출력됩니다.
for 문이 squares에 값을 하나 요청하면, squares는 numbers에 값을 하나 요청합니다. numbers가 1을 만들면 squares가 곧바로 제곱해 1을 내보내고, 그 다음에야 2가 만들어집니다.
즉, 모든 값을 한꺼번에 만든 다음 제곱하는 것이 아니라 값 하나가 파이프라인 전체를 통과한 뒤 다음 값이 만들어집니다. 그러므로 중간 결과를 담는 리스트가 필요 없어 데이터가 커져도 메모리 사용량이 일정합니다.
생성자 표현식(Generator Expression)
간단한 생성자는 함수를 정의하지 않고 한 줄의 표현식으로 만들 수 있습니다.
제 19강의 리스트 내포와 문법이 같으며, 괄호의 종류만 다릅니다.
import sys
squares = (i ** 2 for i in range(5))
print(type(squares).__name__)
print(sum(i ** 2 for i in range(5)))
print(sys.getsizeof([i for i in range(10000)]), sys.getsizeof((i for i in range(10000))))- 결과
- generator
30
85176 200
리스트 내포(List Comprehension)의 대괄호 []를 소괄호 ()로 바꾸면 생성자 표현식(Generator Expression)이 됩니다.
생성자 표현식은 값을 미리 모두 만들어 두지 않고, 요청할 때마다 하나씩 계산합니다.
그러므로 리스트는 요소 개수만큼 메모리를 사용하지만, 생성자 객체의 크기는 요소 개수와 무관하게 일정합니다.
결과의 85176은 1만 개의 요소에 대한 참조를 담은 리스트의 크기(바이트)이고, 200은 생성자 객체 자체의 크기입니다. 요소를 10만 개로 늘려도 생성자의 크기는 거의 변하지 않습니다.
함수에 유일한 인수로 전달할 때는 sum(i ** 2 for i in range(5))처럼 소괄호를 한 번 생략할 수 있습니다.
- Tip :
sys.getsizeof()의 값은 Python 버전과 실행 환경에 따라 달라질 수 있습니다.
data = [1, 2, 3]
g = (x * 10 for x in data)
data.append(4)
print(list(g))
data = [1, 2, 3]
g = (x * 10 for x in data)
data = [7, 8]
print(list(g), list(g))- 결과
- [10, 20, 30, 40]
[10, 20, 30] []
생성자 표현식은 언제 무엇을 계산하는지 알아 두어야 합니다.
생성자 표현식을 만드는 순간에는 가장 바깥의 in data 부분만 즉시 평가되어, 그 시점의 리스트 객체에 대한 반복자가 저장됩니다. x * 10 계산은 값을 꺼낼 때마다 이루어집니다.
첫 번째 예제는 같은 리스트 객체에 4를 추가했으므로, 나중에 값을 꺼낼 때 40까지 포함됩니다.
두 번째 예제는 data라는 이름에 새 리스트를 대입했을 뿐 원래 리스트는 바뀌지 않았으므로, 생성자는 처음의 [1, 2, 3]을 사용합니다.
또한 생성자 표현식도 반복자이므로, 두 번째 list(g)는 빈 리스트를 반환합니다.
- Tip : 결과를 여러 번 사용하거나 색인으로 접근해야 한다면 리스트 내포를, 한 번만 순회하거나
sum(),max()처럼 바로 집계한다면 생성자 표현식을 사용합니다.
코루틴(Coroutine)
지금까지는 생성자가 바깥으로 값을 내보내기만 했습니다.
yield는 표현식으로도 사용할 수 있어서, 생성자가 멈춰 있는 동안 바깥에서 값을 안으로 넣어 줄 수도 있습니다. 이렇게 값을 주고받는 생성자를 코루틴이라 합니다.
from itertools import count
def coroutine():
for c in count():
value = yield c
print(f"Value:{value}, Count:{c}")
cor = coroutine()
cor.send(None)
cor.send("A")
cor.send(200)
cor.send(50)- 결과
- Value:A, Count:0
Value:200, Count:1
Value:50, Count:2
여태까지 사용하던 함수는 서브루틴(Subroutine) 함수로, 진입 지점이 하나이며 return 키워드를 통해 종료되었습니다.
하지만, 생성자(Generator)는 함수 실행 도중에 일시 정지를 할 수 있으므로, 일시 중지한 시점에서 값을 추가로 입력할 수 있습니다.
즉, 진입 지점이 여러 개로 늘어나게 되며 값을 주고 받을 수도 있게됩니다. 이를 코루틴(Coroutine)이라 합니다.
코루틴 함수에서 value = yield c의 형태로 사용한다면, 생성자 함수로 값을 보낼 수 있습니다.
여기서 값을 보내기 위해서는 먼저 생성자를 첫 번째 yield 구문까지 실행시켜야 합니다. 이를 프라이밍(Priming)이라 합니다.
coroutine() 함수가 생성된 이후, cor.send(None) 또는 next(cor)를 활용해 첫 번째 yield까지 실행시킵니다.
- Tip : 시작하지 않은 생성자에
None이 아닌 값을send()하면TypeError가 발생합니다.
이후, cor.send() 메서드를 통해 값을 전달할 수 있습니다.
send(값)을 호출하면 일시 중지된 yield c 표현식의 결과가 전달한 값이 되어 value에 할당되고, 다음 yield를 만날 때까지 실행이 이어집니다.
그러므로, print() 함수가 실행된 뒤 반복문의 다음 회차에서 yield c를 만나 다시 일시 중지합니다. 이때 send()는 다음 c 값을 반환합니다.
value = yield c 한 줄은 두 번에 나뉘어 실행된다는 점이 중요합니다.
cor.send(None):c가0인 상태에서yield c의 오른쪽 절반, 즉0을 내보내고 멈춥니다.value에는 아직 아무것도 대입되지 않았습니다.cor.send("A"): 멈춰 있던yield c표현식의 값이"A"가 되어 왼쪽 절반인value = "A"가 실행됩니다.Value:A, Count:0을 출력한 뒤c가1이 되고 다시yield c에서 멈춥니다.cor.send(200),cor.send(50)도 같은 방식으로 실행되어Count가1,2인 줄을 출력합니다.
결과에 Value:None인 줄이 없는 이유는, 첫 번째 send(None)이 대입이 일어나기 전에 멈추기 때문입니다.
- Tip :
next(cor)는cor.send(None)과 같습니다.
def averager():
while True:
value = yield
avg = averager()
try:
avg.send(10)
except TypeError as e:
print(type(e).__name__, ":", e)- 결과
- TypeError : can’t send non-None value to a just-started generator
프라이밍을 하지 않고 바로 값을 보내면 발생하는 오류입니다.
GEN_CREATED 상태의 생성자는 아직 어떤 yield에서도 멈춰 있지 않으므로, 보낸 값을 받을 yield 표현식이 없습니다. 그러므로 첫 번째 send()에는 반드시 None을 전달하거나 next()를 사용합니다.
def averager():
total = 0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg = averager()
print(next(avg))
print(avg.send(10))
print(avg.send(20))
print(avg.send(60))- 결과
- None
10.0
15.0
30.0
코루틴을 활용해 누적 평균을 계산하는 실용 예제입니다.
next(avg)로 프라이밍하면 첫 번째 yield average에서 아직 평균이 없으므로 None을 내보냅니다.
이후 send(10)이 value에 10을 넣으면 합계와 개수가 갱신되어 평균 10.0을 계산하고, 다음 회차의 yield average에서 그 값을 반환하며 멈춥니다. send(20)은 (10 + 20) / 2 = 15.0, send(60)은 (10 + 20 + 60) / 3 = 30.0을 반환합니다.
합계 total과 개수 count는 전역 변수나 클래스 없이도 생성자의 지역 변수로 호출 사이에 계속 유지됩니다.
from itertools import count
def coroutine():
for c in count():
value = yield c
print(f"send() - Value:{value}, Count:{c}")
yield value
print(f"next() - Value:{value}, Count:{c}")
cor = coroutine()
next(cor)
print(cor.send("A"))
print(next(cor))
print(cor.send(200))
print(next(cor))
print(cor.send(50))
print(next(cor))- 결과
- send() - Value:A, Count:0
A
next() - Value:A, Count:0
1
send() - Value:200, Count:1
200
next() - Value:200, Count:1
2
send() - Value:50, Count:2
50
next() - Value:50, Count:2
3
value에 yield c를 할당한 이후에 yield 키워드를 추가해 반이중 방식(half-duplex) 형태와 흡사한 함수를 구현할 수 있습니다.
cor.send() 메서드를 통해 값을 전달하는 방식과 동일하지만, 함수 내부에 yield value가 추가되어 주고 받는 형식이 됩니다.
send() 메서드는 value에 값을 전달한 뒤, 다음 번째 yield value를 만나기 전까지 모든 구문을 실행합니다.
yield value에서 일시 중지된 상태는 next() 함수를 통해 넘어갈 수 있으며, 이때 두 번째 print()가 실행되고 다음 yield c에서 다시 중지합니다.
즉, 값을 보낼때는 cor.send()를 사용하며, 다음 입력을 받을 위치로 이동할 때는 next()를 사용한다 볼 수 있습니다.
결과의 네 줄씩이 한 회차입니다. send("A")가 첫 줄을 출력하고 A를 반환하며, next(cor)가 셋째 줄을 출력하고 다음 회차의 c 값 1을 반환합니다.
yield value에서 멈춘 상태에서 next() 대신 send()를 호출하면, 보낸 값은 yield value 표현식의 결과로 버려지므로 주의합니다.
- Tip :
cor.send()의 반환값은value값을 반환하며,next(cor)의 반환값은c값을 반환합니다.
오류 발생(throw)
send()가 멈춰 있는 yield에 값을 넣는 것이라면, throw()는 멈춰 있는 yield에 예외를 넣는 것입니다.
바깥에서 “입력이 잘못되었다”, “작업을 다시 시작하라” 같은 상황을 생성자 내부에 알릴 때 사용합니다.
from itertools import count
def coroutine():
for c in count():
try:
value = yield c
print(f"send() - Value:{value}, Count:{c}")
yield value
print(f"next() - Value:{value}, Count:{c}")
except ValueError:
print("Error", c)
cor = coroutine()
next(cor)
cor.send("A")
cor.throw(ValueError("오류가 발생했습니다."))
cor.send(200)
cor.throw(ValueError("오류가 발생했습니다."))
cor.throw(ValueError("오류가 발생했습니다."))- 결과
- send() - Value:A, Count:0
Error 0
send() - Value:200, Count:1
Error 1
Error 2
코루틴(Coroutine) 도중 오류를 강제로 발생시켜, 예외 처리를 진행할 수 있습니다.
cor.throw(예외)를 통해, 생성자가 일시 중지된 yield 위치에서 강제로 오류를 발생시킬 수 있습니다.
생성자 내부에서 예외를 처리하면 실행이 이어지고, 다음 yield에서 다시 중지합니다. 그러므로 Count의 값은 증가하며, next() 함수와 비슷하게 동작합니다.
예제를 따라가면, send("A") 이후 생성자는 yield value에서 멈춰 있습니다. 첫 번째 throw()는 그 위치에서 ValueError를 발생시키고, except 블록이 Error 0을 출력한 뒤 다음 회차의 yield c(c는 1)에서 멈춥니다.
send(200)은 send() - Value:200, Count:1을 출력하고 yield value에서 멈추며, 두 번째 throw()가 Error 1을 출력합니다. 세 번째 throw()는 yield c(c는 2)에서 멈춘 상태에 예외를 넣었으므로 Error 2를 출력합니다.
생성자 내부에서 예외를 처리하지 않으면, 예외는 throw()를 호출한 쪽으로 전파되고 생성자는 종료됩니다.
즉, 입력 → 출력, 입력 → 오류, 입력 → 출력 구조의 형태로도 구현이 가능합니다.
도중에 오류를 발생시키는 이유는, 외부에서 생성자 내부로 특정 상황을 알리고 생성자가 그에 맞게 처리하도록 하기 위함입니다.
이를 통해 특정 오류 상황을 알릴 수 있으며, 코드가 더 간결해질 수 있습니다.
- Tip :
cor.throw(ValueError, "메시지")처럼 예외 형식과 값을 따로 전달하는 방식은 Python 3.12부터 비권장(Deprecated)되어DeprecationWarning이 발생합니다.cor.throw(ValueError("메시지"))처럼 예외 인스턴스 하나만 전달합니다.
def gen():
yield 1
yield 2
g = gen()
print(next(g))
try:
g.throw(ValueError("중단"))
except ValueError as e:
print("밖에서 받은 예외 :", e)
print(next(g, "END"))- 결과
- 1
밖에서 받은 예외 : 중단
END
생성자 안에 try 문이 없는 경우입니다.
throw()로 넣은 예외를 생성자가 처리하지 못하므로, 예외는 생성자 밖으로 빠져나와 throw()를 호출한 코드에서 잡힙니다.
이때 생성자는 종료되므로, 아직 실행되지 않은 yield 2가 남아 있어도 다음 next()는 END를 반환합니다.
생성자(Generator) 종료하기
무한히 값을 만드는 생성자는 스스로 끝나지 않습니다. 더 이상 사용하지 않을 생성자는 close()로 명시적으로 종료할 수 있으며, 이때 생성자 안의 정리 코드가 실행될 기회를 얻습니다.
from itertools import count
def coroutine():
for c in count():
try:
value = yield c
print(f"send() - Value:{value}, Count:{c}")
yield value
print(f"next() - Value:{value}, Count:{c}")
except ValueError:
print("Error", c)
cor = coroutine()
next(cor)
cor.send("A")
cor.close()
print(next(cor, "END"))- 결과
- send() - Value:A, Count:0
END
현재 생성자(Generator)는 무한히 반복되는 구조입니다.
코드 상에 종료 구문이 없으므로 끝이 존재하지 않습니다.
하지만, close() 메서드를 통해 강제로 생성자를 종료할 수 있습니다.
close()는 일시 중지된 yield 위치에서 GeneratorExit 예외를 발생시켜 생성자를 종료합니다. 생성자 안의 finally 블록이 있다면 이때 실행됩니다.
예제의 except ValueError는 GeneratorExit을 처리하지 않으므로, 예외가 그대로 생성자를 빠져나가며 생성자가 종료됩니다.
cor.close()가 호출된 이후 부터는 next()를 호출하면 StopIteration 예외를 발생시킵니다.
즉, 더 이상 참조할 수 없는 형태로 변경합니다.
import inspect
def gen():
try:
yield 1
yield 2
finally:
print("정리 작업")
g = gen()
print(next(g))
g.close()
print(inspect.getgeneratorstate(g))
g.close()- 결과
- 1
정리 작업
GEN_CLOSED
yield 1에서 멈춰 있는 생성자에 close()를 호출하면, 그 위치에서 GeneratorExit이 발생해 finally 블록의 정리 작업이 출력되고 생성자는 GEN_CLOSED 상태가 됩니다.
파일이나 네트워크 연결처럼 반드시 정리해야 하는 자원을 생성자 안에서 사용한다면, finally 블록이나 with 문으로 정리 코드를 작성해 두면 생성자가 중간에 닫혀도 안전하게 정리됩니다.
이미 닫힌 생성자에 close()를 다시 호출해도 아무 일도 일어나지 않습니다.
- Tip : 생성자 객체가 더 이상 참조되지 않아 메모리에서 해제될 때에도
close()가 자동으로 호출됩니다.
def gen():
try:
yield 1
except GeneratorExit:
yield 2
g = gen()
next(g)
try:
g.close()
except RuntimeError as e:
print(type(e).__name__, ":", e)- 결과
- RuntimeError : generator ignored GeneratorExit
GeneratorExit을 잡은 뒤 다시 yield로 값을 내보내면, 종료 요청을 무시한 것으로 간주되어 RuntimeError가 발생합니다.
GeneratorExit은 “정리하고 끝내라”는 신호이므로, 잡더라도 정리 작업만 수행하고 함수를 끝내거나 예외를 다시 발생시켜야 합니다.
- Tip :
async def로 정의하는비동기 코루틴은 이 강좌의 생성자 기반 코루틴에서 발전한 문법이며, 제 51강에서 다룹니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
| yield 값 | 값을 내보내고 일시 정지 | 다음 next()에서 바로 다음 줄부터 재개 |
| yield from 객체 | 반복 가능한 객체나 하위 생성자에 위임 | 하위 생성자의 return 값이 결과 |
| return 값 | 생성자 종료 | StopIteration.value에 저장 |
| (식 for 변수 in 객체) | 생성자 표현식 | 크기가 일정한 생성자 객체 |
| next(생성자) | 다음 yield까지 실행 |
yield한 값 |
| 생성자.send(값) | 멈춘 yield의 결과로 값을 전달하고 재개 |
다음 yield한 값 |
| 생성자.throw(예외) | 멈춘 yield 위치에서 예외 발생 |
처리되면 다음 yield한 값 |
| 생성자.close() | 멈춘 위치에서 GeneratorExit 발생 |
finally 실행 후 종료 |
- 생성자 함수를 호출하면 본문은 실행되지 않고 생성자 객체만 만들어집니다.
- 생성자는
yield에서 멈출 때 지역 변수와 실행 위치(프레임) 를 보관하고, 다음 요청에서 그대로 이어서 실행합니다. - 생성자도 반복자이므로 한 번만 순회할 수 있습니다.
- 값을 보내는 코루틴은 먼저
next()또는send(None)으로 첫 번째yield까지 실행해야 합니다.
댓글 남기기