이번에는 클래스로 데코레이터를 만드는 방법을 알아보겠습니다. 특히 클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는__call__메서드를 구현해야 합니다.
다음은 함수의 시작과 끝을 출력하는 데코레이터입니다.
decorator_class.py
classTrace:def__init__(self,func):# 호출할 함수를 인스턴스의 초깃값으로 받음self.func=func# 호출할 함수를 속성 func에 저장def__call__(self):print(self.func.__name__,'함수 시작')# __name__으로 함수 이름 출력self.func()# 속성 func에 저장된 함수를 호출print(self.func.__name__,'함수 끝')@Trace# @데코레이터defhello():print('hello')hello()# 함수를 그대로 호출
실행 결과
hello 함수 시작
hello
hello 함수 끝
클래스로 데코레이터를 만들 때는 먼저 __init__ 메서드를 만들고 호출할 함수를 초깃값으로 받습니다. 그리고 매개변수로 받은 함수를 속성으로 저장합니다.
classTrace:def__init__(self,func):# 호출할 함수를 인스턴스의 초깃값으로 받음self.func=func# 호출할 함수를 속성 func에 저장
이제 인스턴스를 호출할 수 있도록 __call__ 메서드를 만듭니다. __call__ 메서드에서는 함수의 시작을 알리는 문자열을 출력하고, 속성 func에 저장된 함수를 호출합니다. 그다음에 함수의 끝을 알리는 문자열을 출력합니다.
def__call__(self):print(self.func.__name__,'함수 시작')# __name__으로 함수 이름 출력self.func()# 속성 func에 저장된 함수를 호출print(self.func.__name__,'함수 끝')
데코레이터를 사용하는 방법은 클로저 형태의 데코레이터와 같습니다. 호출할 함수 위에 @을 붙이고 데코레이터를 지정하면 됩니다.
@데코레이터def함수이름():코드
@Trace# @데코레이터defhello():print('hello')
@으로 데코레이터를 지정했으므로 함수는 그대로 호출해줍니다.
hello()# 함수를 그대로 호출
참고로 클래스로 만든 데코레이터는 @을 지정하지 않고, 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있습니다. 다음과 같이 데코레이터에 호출할 함수를 넣어서 인스턴스를 생성한 뒤 인스턴스를 호출해주면 됩니다. 즉, 클래스에 __call__ 메서드를 정의했으므로 함수처럼 ( )(괄호)를 붙여서 호출할 수 있습니다.
defhello():# @데코레이터를 지정하지 않음print('hello')trace_hello=Trace(hello)# 데코레이터에 호출할 함수를 넣어서 인스턴스 생성trace_hello()# 인스턴스를 호출. __call__ 메서드가 호출됨
이번에는 매개변수가 있는 데코레이터를 만들어보겠습니다. 이런 방식의 데코레이터는 값을 지정해서 동작을 바꿀 수 있습니다. 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터입니다.
decorator_parameter.py
defis_multiple(x):# 데코레이터가 사용할 매개변수를 지정defreal_decorator(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수의 매개변수와 똑같이 지정r=func(a,b)# func를 호출하고 반환값을 변수에 저장ifr%x==0:# func의 반환값이 x의 배수인지 확인print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__,x))else:print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__,x))returnr# func의 반환값을 반환returnwrapper# wrapper 함수 반환returnreal_decorator# real_decorator 함수 반환@is_multiple(3)# @데코레이터(인수)defadd(a,b):returna+bprint(add(10,20))print(add(2,5))
실행 결과
add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7
실행을 해보면 add 함수의 반환값이 3의 배수인지 아닌지 알려줍니다
지금까지 데코레이터를 만들 때 함수 안에 함수를 하나만 만들었습니다. 하지만 매개변수가 있는 데코레이터를 만들 때는 함수를 하나 더 만들어야 합니다.
먼저 is_multiple 함수를 만들고 데코레이터가 사용할 매개변수 x를 지정합니다. 그리고 is_multiple 함수 안에서 실제 데코레이터 역할을 하는 real_decorator를 만듭니다. 즉, 이 함수에서 호출할 함수를 매개변수로 받습니다. 그다음에 real_decorator 함수 안에서 wrapper 함수를 만들어주면 됩니다.
defis_multiple(x):# 데코레이터가 사용할 매개변수를 지정defreal_decorator(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수의 매개변수와 똑같이 지정
wrapper 함수 안에서는 먼저 func의 결과가 데코레이터 매개변수 x의 배수인지 확인합니다. 그다음에 func의 반환값을 반환합니다.
defis_multiple(x):# 데코레이터가 사용할 매개변수를 지정defreal_decorator(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수의 매개변수와 똑같이 지정r=func(a,b)# func를 호출하고 반환값을 변수에 저장ifr%x==0:# func의 반환값이 x의 배수인지 확인print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__,x))else:print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__,x))returnr# func의 반환값을 반환
여기서는 real_decorator, wrapper 함수를 두 개 만들었으므로 함수를 만든 뒤에 return으로 두 함수를 반환해줍니다.
returnwrapper# wrapper 함수 반환returnreal_decorator# real_decorator 함수 반환
데코레이터를 사용할 때는 데코레이터에 ( )(괄호)를 붙인 뒤 인수를 넣어주면 됩니다.
@데코레이터(인수)def함수이름():코드
@is_multiple(3)# @데코레이터(인수)defadd(a,b):returna+b
여기서는 is_multiple에 3을 지정해서 add 함수의 반환값이 3의 배수인지 확인했습니다. 물론 is_multiple에 다른 숫자를 넣으면 함수의 반환값이 해당 숫자의 배수인지 확인해줍니다.
참고 | 매개변수가 있는 데코레이터를 여러 개 지정하기
매개변수가 있는 데코레이터를 여러 개 지정할 때는 다음과 같이 인수를 넣은 데코레이터를 여러 줄로 지정해줍니다.
데코레이터를 여러 개 사용하면 데코레이터에서 반환된 wrapper 함수가 다른 데코레이터로 들어갑니다. 따라서 함수의 __name__을 출력해보면 wrapper가 나옵니다.
add의반환값은7의배수가아닙니다.wrapper의반환값은3의배수입니다.
함수의 원래 이름을 출력하고 싶다면 functools 모듈의 wraps 데코레이터를 사용해야 합니다. 다음과 같이 @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정해줍니다(from functools import wraps로 데코레이터를 가져왔다면 @wraps(func)를 지정).
decorator_functools_wraps.py
importfunctoolsdefis_multiple(x):defreal_decorator(func):@functools.wraps(func)# @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정defwrapper(a,b):r=func(a,b)ifr%x==0:print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__,x))else:print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__,x))returnrreturnwrapperreturnreal_decorator@is_multiple(3)@is_multiple(7)defadd(a,b):returna+badd(10,20)
실행 결과
add의 반환값은 7의 배수가 아닙니다.
add의 반환값은 3의 배수입니다.
@functools.wraps는 원래 함수의 정보를 유지시켜줍니다. 따라서 디버깅을 할 때 유용하므로 데코레이터를 만들 때는 @functools.wraps를 사용하는 것이 좋습니다.
지금까지 매개변수와 반환값이 없는 함수의 데코레이터를 만들었습니다. 이번에는 매개변수와 반환값을 처리하는 데코레이터는 어떻게 만드는지 알아보겠습니다. 다음은 함수의 매개변수와 반환값을 출력하는 데코레이터입니다.
decorator_param_return.py
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수 add(a, b)의 매개변수와 똑같이 지정r=func(a,b)# func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장print(f'{func.__name__}(a={a}, b={b}) -> {r}')# 매개변수와 반환값 출력returnr# func의 반환값을 반환returnwrapper# wrapper 함수 반환@trace# @데코레이터defadd(a,b):# 매개변수는 두 개returna+b# 매개변수 두 개를 더해서 반환print(add(10,20))
실행 결과
add(a=10, b=20) -> 30
30
add 함수를 호출했을 때 데코레이터를 통해서 매개변수와 반환값이 출력되었습니다. 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 안쪽 wrapper 함수의 매개변수를 호출할 함수 add(a, b)의 매개변수와 똑같이 만들어줍니다.
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
wrapper 함수 안에서는 func를 호출하고 반환값을 변수에 저장합니다. 그다음에 print로 매개변수와 반환값을 출력합니다. 이때 func에는 매개변수 a와 b를 그대로 넣어줍니다. 또한, add 함수는 두 수를 더해서 반환해야 하므로 func의 반환값을 return으로 반환해줍니다.
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper(a,b):# 호출할 함수 add(a, b)의 매개변수와 똑같이 지정r=func(a,b)# func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장print(f'{func.__name__}(a={a}, b={b}) -> {r}')# 매개변수와 반환값 출력returnr# func의 반환값을 반환returnwrapper# wrapper 함수 반환
만약 wrapper 함수에서 func의 반환값을 반환하지 않으면 add 함수를 호출해도 반환값이 나오지 않으므로 주의해야 합니다. 참고로 wrapper 함수에서 func의 반환값을 출력할 필요가 없으면 return func(a, b)처럼 func를 호출하면서 바로 반환해도 됩니다.
데코레이터를 사용할 때는 @로 함수 위에 지정해주면 됩니다. 또한, @로 데코레이터를 사용했으므로 add 함수는 그대로 호출해줍니다.
@trace# @데코레이터defadd(a,b):# 매개변수는 두 개returna+b# 매개변수 두 개를 더해서 반환
42.2.1가변 인수 함수 데코레이터
def add(a, b):는 매개변수의 개수가 고정된 함수입니다. 그러면 매개변수(인수)가 고정되지 않은 함수는 어떻게 처리할까요? 이때는 wrapper 함수를 가변 인수 함수로 만들면 됩니다.
decorator_variable_argument.py
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper(*args,**kwargs):# 가변 인수 함수로 만듦r=func(*args,**kwargs)# func에 args, kwargs를 언패킹하여 넣어줌print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {r}')# 매개변수와 반환값 출력returnr# func의 반환값을 반환returnwrapper# wrapper 함수 반환@trace# @데코레이터defget_max(*args):# 위치 인수를 사용하는 가변 인수 함수returnmax(args)@trace# @데코레이터defget_min(**kwargs):# 키워드 인수를 사용하는 가변 인수 함수returnmin(kwargs.values())print(get_max(10,20))print(get_min(x=10,y=20,z=30))
클래스를 만들면서 메서드에 데코레이터를 사용할 때는 self를 주의해야 합니다. 인스턴스 메서드는 항상 self를 받으므로 데코레이터를 만들 때도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 합니다(클래스 메서드는 cls). 마찬가지로 func를 호출할 때도 self와 매개변수를 그대로 넣어야 합니다.
decorator_method.py
deftrace(func):defwrapper(self,a,b):# 호출할 함수가 인스턴스 메서드이므로 첫 번째 매개변수는 self로 지정r=func(self,a,b)# self와 매개변수를 그대로 넣어줌print(f'{func.__name__}(a={a}, b={b}) -> {r}')# 매개변수와 반환값 출력returnr# func의 반환값을 반환returnwrapperclassCalc:@tracedefadd(self,a,b):# add는 인스턴스 메서드returna+bc=Calc()print(c.add(10,20))
데코레이터는 함수를 장식한다고 했는데 도대체 어디에 사용하는 것일까요? 데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용합니다. 예를 들어서 함수의 시작과 끝을 출력하고 싶다면 다음과 같이 함수 시작, 끝 부분에 print를 넣어주어야 합니다.
function_begin_end.py
defhello():print('hello 함수 시작')print('hello')print('hello 함수 끝')defworld():print('world 함수 시작')print('world')print('world 함수 끝')hello()world()
실행 결과
hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝
만약 다른 함수도 시작과 끝을 출력하고 싶다면 함수를 만들 때마다 print를 넣어야 합니다. 따라서 함수가 많아지면 매우 번거로워집니다.
이런 경우에는 데코레이터를 활용하면 편리합니다. 다음은 함수의 시작과 끝을 출력하는 데코레이터입니다.
데코레이터는 기본적으로 클로저의 개념을 포함합니다. 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다.
아래의 코드가 이해가 잘 되지 않으신다면 클로저를 보고오세요.
decorator_closure.py
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper():# 호출할 함수를 감싸는 함수print(func.__name__,'함수 시작')# __name__으로 함수 이름 출력func()# 매개변수로 받은 함수를 호출print(func.__name__,'함수 끝')returnwrapper# wrapper 함수 반환defhello():print('hello')defworld():print('world')trace_hello=trace(hello)# 데코레이터에 호출할 함수를 넣음trace_hello()# 반환된 함수를 호출trace_world=trace(world)# 데코레이터에 호출할 함수를 넣음trace_world()# 반환된 함수를 호출
실행 결과
hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝
hello와 world 함수의 시작과 끝이 출력되었습니다. 먼저 데코레이터 trace는 호출할 함수를 매개변수로 받습니다(trace는 추적하다라는 뜻인데 프로그래밍에서 함수의 실행 상황을 추적할 때 trace라는 말을 사용합니다).
deftrace(func):# 호출할 함수를 매개변수로 받음
trace 함수 안에서는 호출할 함수를 감싸는 함수 wrapper를 만듭니다(wrapper는 물건을 싸는 포장지라는 뜻인데 여기서는 함수를 감싼다고 해서 이런 이름을 붙였습니다. 다른 이름을 사용해도 상관없습니다).
defwrapper():# 호출할 함수를 감싸는 함수
이제 wrapper 함수에서는 함수의 시작을 알리는 문자열을 출력하고, trace에서 매개변수로 받은 func를 호출합니다. 그다음에 함수의 끝을 알리는 문자열을 출력합니다. 여기서 매개변수로 받은 함수의 원래 이름을 출력할 때는 __name__ 속성을 활용합니다. 마지막으로 wrapper 함수를 다 만들었으면 return을 사용하여 wrapper 함수 자체를 반환합니다.
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper():# 호출할 함수를 감싸는 함수print(func.__name__,'함수 시작')# __name__으로 함수 이름 출력func()# 매개변수로 받은 함수를 호출print(func.__name__,'함수 끝')returnwrapper# wrapper 함수 반환
즉, 함수 안에서 함수를 만들고 반환하는 클로저입니다.
데코레이터를 사용할 때는 trace에 호출할 함수 hello 또는 world를 넣습니다. 그다음에 데코레이터에서 반환된 함수를 호출합니다. 이렇게 하면 데코레이터에 인수로 넣은 함수를 호출하고 시작과 끝을 출력합니다.
trace_hello=trace(hello)# 데코레이터에 호출할 함수를 넣음trace_hello()# 반환된 함수를 호출trace_world=trace(world)# 데코레이터에 호출할 함수를 넣음trace_world()# 반환된 함수를 호출
물론 trace에 다른 함수를 넣은 뒤 반환된 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있습니다.
42.1.1@로 데코레이터 사용하기
이제 @을 사용하여 좀 더 간편하게 데코레이터를 사용해보겠습니다. 다음과 같이 호출할 함수 위에 @데코레이터 형식으로 지정합니다.
@데코레이터def함수이름():코드
decorator_closure_at_sign.py
deftrace(func):# 호출할 함수를 매개변수로 받음defwrapper():print(func.__name__,'함수 시작')# __name__으로 함수 이름 출력func()# 매개변수로 받은 함수를 호출print(func.__name__,'함수 끝')returnwrapper# wrapper 함수 반환@trace# @데코레이터defhello():print('hello')@trace# @데코레이터defworld():print('world')hello()# 함수를 그대로 호출world()# 함수를 그대로 호출
실행 결과
hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝
hello와 world 함수 위에 @trace 를 붙인 뒤에 hello와 world 함수를 그대로 호출하면 끝입니다.
@trace# @데코레이터defhello():print('hello')@trace# @데코레이터defworld():print('world')hello()# 함수를 그대로 호출world()# 함수를 그대로 호출
물론 다른 함수 위에 @trace를 붙인 뒤 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있습니다.
이 데코레이터를 그림으로 표현하면 다음과 같은 모양이 됩니다.
이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있습니다. 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용합니다.
참고 | 데코레이터를 여러 개 지정하기
함수에는 데코레이터를 여러 개 지정할 수 있습니다. 다음과 같이 함수 위에 데코레이터를 여러 줄로 지정해줍니다. 이때 데코레이터가 실행되는 순서는 위에서 아래 순입니다.
@데코레이터1@데코레이터2def함수이름():코드
multiple_decorators.py
defdecorator1(func):defwrapper():print('decorator1')func()returnwrapperdefdecorator2(func):defwrapper():print('decorator2')func()returnwrapper# 데코레이터를 여러 개 지정@decorator1@decorator2defhello():print('hello')hello()
defnumber_generator():x=[1,2,3]yield fromx# 리스트에 들어있는 요소를 한 개씩 바깥으로 전달foriinnumber_generator():print(i)
실행 결과
1
2
3
yield from x와 같이 yield from에 리스트(반복 가능한 객체)를 지정했습니다. 이렇게 하면 리스트에 들어있는 요소를 한 개씩 바깥으로 전달합니다. 즉, yield from을 한 번 사용하여 값을 세 번 바깥으로 전달합니다. 따라서 next 함수(__next__ 메서드)를 세 번 호출할 수 있습니다.
이번에는 yield from에 제너레이터 객체를 지정해보겠습니다(이터레이터는 제너레이터와 동작이 같으므로 생략하겠습니다).
generator_yield_from_generator.py
defnumber_generator(stop):n=0whilen<stop:yieldnn+=1defthree_generator():yield fromnumber_generator(3)# 숫자를 세 번 바깥으로 전달foriinthree_generator():print(i)
실행 결과
0
1
2
먼저 제너레이터 number_generator는 매개변수로 받은 숫자 직전까지 숫자를 만들어냅니다. 그리고 three_generator에서는 yield from number_generator(3)과 같이 yield from에 제너레이터 객체를 지정했습니다.
number_generator(3)은 숫자를 세 개를 만들어내므로 yield from number_generator(3)은 숫자를 세 번 바깥으로 전달합니다. 따라서 for 반복문에 three_generator()를 사용하면 숫자를 세 번 출력합니다(next 함수 또는 __next__ 메서드도 세 번 호출 가능).
이번 유닛에서는 제너레이터와 yield 사용 방법을 알아보았습니다. 이 부분은 파이썬에서도 고급 기능에 해당하는 부분이라 초보자들은 이해하기가 쉽지 않습니다. 그래서 지금 당장 yield가 이해되지 않는다고 해서 걱정할 필요가 없습니다. 나중에 프로그래밍에 익숙해지면 자연스럽게 익히게 됩니다.
참고 | 제너레이터 표현식
리스트 표현식을 사용할 때 [ ](대괄호)를 사용했습니다. 같은 리스트 표현식을 ( )(괄호)로 묶으면 제너레이터 표현식이 됩니다. 리스트 표현식은 처음부터 리스트의 요소를 만들어내지만 제너레이터 표현식은 필요할 때 요소를 만들어내므로 메모리를 절약할 수 있습니다.
제너레이터와yield에 대해 알아보았으니 이번에는range(횟수)처럼 동작을 하는 제너레이터를 만들어보겠습니다.
generator.py
defnumber_generator(stop):n=0# 숫자는 0부터 시작whilen<stop:# 현재 숫자가 반복을 끝낼 숫자보다 작을 때 반복yieldn# 현재 숫자를 바깥으로 전달n+=1# 현재 숫자를 증가시킴foriinnumber_generator(3):print(i)
실행 결과
0
1
2
코드는 간단합니다. 제너레이터 안에서 변수 n을 만들고 0을 저장합니다. 그리고 while n < stop:과 같이 반복을 끝낼 숫자보다 작을 때 반복하도록 만듭니다. 반복문 안에서는 yield n으로 숫자를 바깥으로 전달한 뒤 n을 1 증가시키면 됩니다. 여기서는 yield가 3번 나오므로 for 반복문도 3번 반복합니다.
그럼 yield에서 함수(메서드)를 호출하면 어떻게 될까요? 다음은 리스트에 들어있는 문자열을 대문자로 변환하여 함수 바깥으로 전달합니다.
generator_yield_function.py
defupper_generator(x):foriinx:yieldi.upper()# 함수의 반환값을 바깥으로 전달fruits=['apple','pear','grape','pineapple','orange']foriinupper_generator(fruits):print(i)
실행 결과
APPLE
PEAR
GRAPE
PINEAPPLE
ORANGE
리스트 fruits에 들어있는 문자열이 모두 대문자로 출력되었습니다.yield i.upper()와 같이 yield에서 함수(메서드)를 호출하면 해당 함수의 반환값을 바깥으로 전달합니다. upper는 호출했을 때 대문자로 된 문자열을 반환하므로 yield는 이 문자열을 바깥으로 전달합니다. 즉, yield에 무엇을 지정하든 결과만 바깥으로 전달합니다(함수의 반환값, 식의 결과).
제너레이터는 이터레이터를 생성해주는 함수입니다. 이터레이터는 클래스에 __iter__, __next__ 또는 __getitem__ 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝입니다. 그래서 제너레이터는 이터레이터보다 훨씬 간단하게 작성할 수 있습니다.
참고로 제너레이터는 발생자라고 부르기도 합니다. 이 책에서는 제너레이터를 사용하겠습니다.
40.1 제너레이터와 yield 알아보기
함수 안에서 yield를 사용하면 함수는 제너레이터가 되며 yield에는 값(변수)을 지정합니다.
yield 값
이제 yield를 사용해서 제너레이터를 만들고 for 반복문에서 0, 1, 2 숫자 세 개를 출력해보겠습니다.
이처럼 함수에 yield만 사용해서 간단하게 이터레이터를 구현할 수 있습니다. 단, 이터레이터는 __next__ 메서드 안에서 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드(next 함수)의 반환값으로 나옵니다. 또한, 이터레이터는 raise로 StopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 자동으로 발생합니다.
제너레이터는 제너레이터 객체에서 __next__ 메서드를 호출할 때마다 함수 안의 yield까지 코드를 실행하며 yield에서 값을 발생시킵니다(generate). 그래서 이름이 제너레이터(generator)입니다.
40.1.2for와 제너레이터
그럼 for 반복문과 제너레이터를 살펴보겠습니다. 다음과 같이 for 반복문은 반복할 때마다 __next__를 호출하므로 yield에서 발생시킨 값을 가져옵니다. 그리고 StopIteration 예외가 발생하면 반복을 끝냅니다.
참고로 제너레이터 객체에서 __iter__를 호출하면 self를 반환하므로 같은 객체가 나옵니다(제너레이터 함수 호출 > 제너레이터 객체 > __iter__는 self 반환 > 제너레이터 객체).
그런데 generate라는 키워드를 사용하면 되지 왜 yield라고 이름을 지었을까요? yield는 생산하다라는 뜻과 함께 양보하다라는 뜻도 가지고 있습니다. 즉, yield를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보합니다. 따라서yield는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만듭니다.
40.1.3yield의 동작 과정 알아보기
그럼 yield의 동작 과정을 알아보기 위해 for 반복문 대신 next 함수로 __next__ 메서드를 직접 호출해보겠습니다.
변수 = next(제너레이터객체)
yield_next.py
defnumber_generator():yield0# 0을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보yield1# 1을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보yield2# 2를 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보g=number_generator()a=next(g)# yield를 사용하여 함수 바깥으로 전달한 값은 next의 반환값으로 나옴print(a)# 0b=next(g)print(b)# 1c=next(g)print(c)# 2
실행 결과
0
1
2
yield를 사용하여 바깥으로 전달한 값은 next 함수(__next__ 메서드)의 반환값으로 나온다고 했습니다. 따라서 next(g)의 반환값을 출력해보면 yield에 지정한 값 0, 1, 2가 차례대로 나옵니다. 즉, 제너레이터 함수가 실행되는 중간에 next로 값을 가져옵니다.
next와 yield의 동작 과정을 그림으로 살펴보겠습니다.
먼저 g = number_generator()와 같이 제너레이터 객체를 만듭니다. 그다음에 next(g)를 호출하면 제너레이터 안의 yield 0이 실행되어 숫자 0을 전달한 뒤 바깥의 코드가 실행되도록 양보합니다. 함수 바깥에서는 print(a)로 next(g)에서 반환된 값을 출력합니다.
값을 출력했으면 next(g)로 다시 제너레이터 안의 코드를 실행합니다. 이때는 yield 1이 실행되고 숫자 1을 발생시켜서 바깥으로 전달합니다. 그리고 함수 바깥에서는 print(b)로 next(g)에서 반환된 값을 출력합니다.
마찬가지로 과정으로 yield 2도 숫자를 발생시키고 print(c)로 제너레이터에서 나온 값을 출력합니다.
이렇게 제너레이터는 함수를 끝내지 않은 상태에서 yield를 사용하여 값을 바깥으로 전달할 수 있습니다. 즉, return은 반환 즉시 함수가 끝나지만 yield는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식입니다.
참고 | 제너레이터와 return
제너레이터는 함수 끝까지 도달하면 StopIteration 예외가 발생합니다. 마찬가지로 return도 함수를 끝내므로 return을 사용해서 함수 중간에 빠져나오면 StopIteration 예외가 발생합니다.
특히 제너레이터 안에서 return에 반환값을 지정하면 StopIteration 예외의 에러 메시지로 들어갑니다.
generator_return.py
defone_generator():yield1return'return에 지정한 값'try:g=one_generator()next(g)next(g)exceptStopIterationase:print(e)# return에 지정한 값
잘 보면 함수 calc가 끝났는데도 c는 calc의 지역 변수 a, b를 사용해서 계산을 하고 있습니다. 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다. 여기서는 c에 저장된 함수가 클로저입니다.
이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있습니다. 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용합니다. 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용합니다.