https://dojang.io/mod/page/view.php?id=2430


42.4 클래스로 데코레이터 만들기

이번에는 클래스로 데코레이터를 만드는 방법을 알아보겠습니다. 특히 클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는 __call__ 메서드를 구현해야 합니다.

다음은 함수의 시작과 끝을 출력하는 데코레이터입니다.

decorator_class.py

class Trace:
    def __init__(self, func):    # 호출할 함수를 인스턴스의 초깃값으로 받음
        self.func = func         # 호출할 함수를 속성 func에 저장
 
    def __call__(self):
        print(self.func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        self.func()                               # 속성 func에 저장된 함수를 호출
        print(self.func.__name__, '함수 끝')
 
@Trace    # @데코레이터
def hello():
    print('hello')
 
hello()    # 함수를 그대로 호출

실행 결과

hello 함수 시작
hello
hello 함수 끝

클래스로 데코레이터를 만들 때는 먼저 __init__ 메서드를 만들고 호출할 함수를 초깃값으로 받습니다. 그리고 매개변수로 받은 함수를 속성으로 저장합니다.

class Trace:
    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    # @데코레이터
def hello():
    print('hello')

@으로 데코레이터를 지정했으므로 함수는 그대로 호출해줍니다.

hello()    # 함수를 그대로 호출

참고로 클래스로 만든 데코레이터는 @을 지정하지 않고, 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있습니다. 다음과 같이 데코레이터에 호출할 함수를 넣어서 인스턴스를 생성한 뒤 인스턴스를 호출해주면 됩니다. 즉, 클래스에 __call__ 메서드를 정의했으므로 함수처럼 ( )(괄호)를 붙여서 호출할 수 있습니다.

def hello():    # @데코레이터를 지정하지 않음
    print('hello')
 
trace_hello = Trace(hello)    # 데코레이터에 호출할 함수를 넣어서 인스턴스 생성
trace_hello()                 # 인스턴스를 호출. __call__ 메서드가 호출됨



https://dojang.io/mod/page/view.php?id=2429

42.3 매개변수가 있는 데코레이터 만들기

이번에는 매개변수가 있는 데코레이터를 만들어보겠습니다. 이런 방식의 데코레이터는 값을 지정해서 동작을 바꿀 수 있습니다. 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터입니다.

decorator_parameter.py

def is_multiple(x):              # 데코레이터가 사용할 매개변수를 지정
    def real_decorator(func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정
            r = func(a, b)       # func를 호출하고 반환값을 변수에 저장
            if r % x == 0:       # func의 반환값이 x의 배수인지 확인
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r             # func의 반환값을 반환
        return wrapper           # wrapper 함수 반환
    return real_decorator        # real_decorator 함수 반환
 
@is_multiple(3)     # @데코레이터(인수)
def add(a, b):
    return a + b
 
print(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 함수를 만들어주면 됩니다.

def is_multiple(x):              # 데코레이터가 사용할 매개변수를 지정
    def real_decorator(func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정

wrapper 함수 안에서는 먼저 func의 결과가 데코레이터 매개변수 x의 배수인지 확인합니다. 그다음에 func의 반환값을 반환합니다.

def is_multiple(x):              # 데코레이터가 사용할 매개변수를 지정
    def real_decorator(func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정
            r = func(a, b)       # func를 호출하고 반환값을 변수에 저장
            if r % x == 0:       # func의 반환값이 x의 배수인지 확인
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r             # func의 반환값을 반환

여기서는 real_decoratorwrapper 함수를 두 개 만들었으므로 함수를 만든 뒤에 return으로 두 함수를 반환해줍니다.

        return wrapper           # wrapper 함수 반환
    return real_decorator        # real_decorator 함수 반환

데코레이터를 사용할 때는 데코레이터에 ( )(괄호)를 붙인 뒤 인수를 넣어주면 됩니다.

@데코레이터(인수)
def 함수이름():
    코드
@is_multiple(3)     # @데코레이터(인수)
def add(a, b):
    return a + b

여기서는 is_multiple에 3을 지정해서 add 함수의 반환값이 3의 배수인지 확인했습니다. 물론 is_multiple에 다른 숫자를 넣으면 함수의 반환값이 해당 숫자의 배수인지 확인해줍니다.

참고 | 매개변수가 있는 데코레이터를 여러 개 지정하기

매개변수가 있는 데코레이터를 여러 개 지정할 때는 다음과 같이 인수를 넣은 데코레이터를 여러 줄로 지정해줍니다.

@데코레이터1(인수)
@데코레이터2(인수)
def 함수이름():
    코드
@is_multiple(3)
@is_multiple(7)
def add(a, b):
    return a + b
 
add(10, 20)

실행 결과

add의 반환값은 7의 배수가 아닙니다.
wrapper의 반환값은 3의 배수입니다.

@을 사용하지 않았을 때는 다음 코드와 동작이 같습니다.

decorated_add = is_multiple(3)(is_multiple(7)(add))
decorated_add(10, 20)
참고 | 원래 함수 이름이 안나온다면?

데코레이터를 여러 개 사용하면 데코레이터에서 반환된 wrapper 함수가 다른 데코레이터로 들어갑니다. 따라서 함수의 __name__을 출력해보면 wrapper가 나옵니다.

add의 반환값은 7 배수가 아닙니다.
wrapper의 반환값은 3 배수입니다.

함수의 원래 이름을 출력하고 싶다면 functools 모듈의 wraps 데코레이터를 사용해야 합니다. 다음과 같이 @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정해줍니다(from functools import wraps로 데코레이터를 가져왔다면 @wraps(func)를 지정).

decorator_functools_wraps.py

import functools
 
def is_multiple(x):
    def real_decorator(func):
        @functools.wraps(func)    # @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정
        def wrapper(a, b):
            r = func(a, b)
            if r % x == 0:
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r
        return wrapper
    return real_decorator
 
@is_multiple(3)
@is_multiple(7)
def add(a, b):
    return a + b
 
add(10, 20)

실행 결과

add의 반환값은 7의 배수가 아닙니다.
add의 반환값은 3의 배수입니다.

@functools.wraps는 원래 함수의 정보를 유지시켜줍니다. 따라서 디버깅을 할 때 유용하므로 데코레이터를 만들 때는 @functools.wraps를 사용하는 것이 좋습니다.



https://dojang.io/mod/page/view.php?id=2428

42.2 매개변수와 반환값을 처리하는 데코레이터 만들기

지금까지 매개변수와 반환값이 없는 함수의 데코레이터를 만들었습니다. 이번에는 매개변수와 반환값을 처리하는 데코레이터는 어떻게 만드는지 알아보겠습니다. 다음은 함수의 매개변수와 반환값을 출력하는 데코레이터입니다.

decorator_param_return.py

def trace(func):          # 호출할 함수를 매개변수로 받음     def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정         r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장         print(f'{func.__name__}(a={a}, b={b}) -> {r}')  # 매개변수와 반환값 출력         return r          # func의 반환값을 반환     return wrapper        # wrapper 함수 반환   @trace              # @데코레이터 def add(a, b):      # 매개변수는 두 개     return a + b    # 매개변수 두 개를 더해서 반환   print(add(10, 20))

실행 결과

add(a=10, b=20) -> 30
30

add 함수를 호출했을 때 데코레이터를 통해서 매개변수와 반환값이 출력되었습니다. 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 안쪽 wrapper 함수의 매개변수를 호출할 함수 add(a, b)의 매개변수와 똑같이 만들어줍니다.

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정

wrapper 함수 안에서는 func를 호출하고 반환값을 변수에 저장합니다. 그다음에 print로 매개변수와 반환값을 출력합니다. 이때 func에는 매개변수 a와 b를 그대로 넣어줍니다. 또한, add 함수는 두 수를 더해서 반환해야 하므로 func의 반환값을 return으로 반환해줍니다.

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
        r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장
        print(f'{func.__name__}(a={a}, b={b}) -> {r}')    # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # wrapper 함수 반환

만약 wrapper 함수에서 func의 반환값을 반환하지 않으면 add 함수를 호출해도 반환값이 나오지 않으므로 주의해야 합니다. 참고로 wrapper 함수에서 func의 반환값을 출력할 필요가 없으면 return func(a, b)처럼 func를 호출하면서 바로 반환해도 됩니다.

데코레이터를 사용할 때는 @로 함수 위에 지정해주면 됩니다. 또한, @로 데코레이터를 사용했으므로 add 함수는 그대로 호출해줍니다.

@trace              # @데코레이터
def add(a, b):      # 매개변수는 두 개
    return a + b    # 매개변수 두 개를 더해서 반환

42.2.1  가변 인수 함수 데코레이터

def add(a, b):는 매개변수의 개수가 고정된 함수입니다. 그러면 매개변수(인수)가 고정되지 않은 함수는 어떻게 처리할까요? 이때는 wrapper 함수를 가변 인수 함수로 만들면 됩니다.

decorator_variable_argument.py

def trace(func):                     # 호출할 함수를 매개변수로 받음     def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦         r = func(*args, **kwargs)    # func에 args, kwargs를 언패킹하여 넣어줌         print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {r}')                                      # 매개변수와 반환값 출력         return r                     # func의 반환값을 반환     return wrapper                   # wrapper 함수 반환   @trace                   # @데코레이터 def get_max(*args):      # 위치 인수를 사용하는 가변 인수 함수     return max(args)   @trace                   # @데코레이터 def get_min(**kwargs):   # 키워드 인수를 사용하는 가변 인수 함수     return min(kwargs.values())   print(get_max(10, 20)) print(get_min(x=10, y=20, z=30))

실행 결과

get_max(args=(10, 20), kwargs={}) -> 20
20
get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
10

get_max 함수와 get_min 함수는 가변 인수 함수입니다. 따라서 데코레이터도 가변 인수 함수로 만들어줍니다. 이때 위치 인수와 키워드 인수를 모두 받을 수 있도록 *args와 **kwargs를 지정해줍니다.

def trace(func):                     # 호출할 함수를 매개변수로 받음
    def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦

wrapper 함수 안에서는 func를 호출해주는데 args는 리스트이고, kwargs는 딕셔너리이므로 func에 넣을 때는 언패킹하여 넣어줍니다. 그리고 print로 매개변수와 반환값을 출력합니다

def trace(func):                     # 호출할 함수를 매개변수로 받음
    def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦
        r = func(*args, **kwargs)    # func에 args, kwargs를 언패킹하여 넣어줌
        print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {r}')
                                     # 매개변수와 반환값 출력
        return r                     # func의 반환값을 반환
    return wrapper                   # wrapper 함수 반환

이렇게 만든 데코레이터 trace는 위치 인수와 키워드 인수를 모두 처리할 수 있습니다. 따라서 가변 인수 함수뿐만 아니라 일반적인 함수에도 사용할 수 있습니다.

>>> @trace
... def add(a, b):
...    return a + b
...
>>> add(10, 20)
add(args=(10, 20), kwargs={}) -> 30
30
참고 | 메서드에 데코레이터 사용하기

클래스를 만들면서 메서드에 데코레이터를 사용할 때는 self를 주의해야 합니다. 인스턴스 메서드는 항상 self를 받으므로 데코레이터를 만들 때도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 합니다(클래스 메서드는 cls). 마찬가지로 func를 호출할 때도 self와 매개변수를 그대로 넣어야 합니다.

decorator_method.py

def trace(func):
    def wrapper(self, a, b):   # 호출할 함수가 인스턴스 메서드이므로 첫 번째 매개변수는 self로 지정
        r = func(self, a, b)   # self와 매개변수를 그대로 넣어줌
        print(f'{func.__name__}(a={a}, b={b}) -> {r}')   # 매개변수와 반환값 출력
        return r               # func의 반환값을 반환
    return wrapper
 
class Calc:
    @trace
    def add(self, a, b):    # add는 인스턴스 메서드
        return a + b
 
c = Calc()
print(c.add(10, 20))

실행 결과

add(a=10, b=20) -> 30
30

https://dojang.io/mod/page/view.php?id=2427


Unit 42. 데코레이터 사용하기

파이썬은 데코레이터(decorator)라는 기능을 제공합니다. 데코레이터는 장식하다, 꾸미다라는 뜻의 decorate에 er(or)을 붙인 말인데 장식하는 도구 정도로 설명할 수 있습니다.

지금까지 클래스에서 메서드를 만들 때 @staticmethod@classmethod@abstractmethod 등을 붙였는데, 이렇게 @로 시작하는 것들이 데코레이터입니다. 즉, 함수(메서드)를 장식한다고 해서 이런 이름이 붙었습니다.

class Calc:
    @staticmethod    # 데코레이터
    def add(a, b):
        print(a + b)

이번 유닛에서는 데코레이터를 만들고 사용하는 방법을 알아보겠습니다.

참고로 데코레이터는 장식자라고 부르기도 합니다. 이 책에서는 데코레이터를 사용하겠습니다.

42.1 데코레이터 만들기

데코레이터는 함수를 장식한다고 했는데 도대체 어디에 사용하는 것일까요? 데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용합니다. 예를 들어서 함수의 시작과 끝을 출력하고 싶다면 다음과 같이 함수 시작, 끝 부분에 print를 넣어주어야 합니다.

function_begin_end.py

def hello():
    print('hello 함수 시작')
    print('hello')
    print('hello 함수 끝')
 
def world():
    print('world 함수 시작')
    print('world')
    print('world 함수 끝')
 
hello()
world()

실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

만약 다른 함수도 시작과 끝을 출력하고 싶다면 함수를 만들 때마다 print를 넣어야 합니다. 따라서 함수가 많아지면 매우 번거로워집니다.

이런 경우에는 데코레이터를 활용하면 편리합니다. 다음은 함수의 시작과 끝을 출력하는 데코레이터입니다.

데코레이터는 기본적으로 클로저의 개념을 포함합니다. 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다.

아래의 코드가 이해가 잘 되지 않으신다면 클로저를 보고오세요.

decorator_closure.py

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
def hello():
    print('hello')
 
def world():
    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라는 말을 사용합니다).

def trace(func):                             # 호출할 함수를 매개변수로 받음

trace 함수 안에서는 호출할 함수를 감싸는 함수 wrapper를 만듭니다(wrapper는 물건을 싸는 포장지라는 뜻인데 여기서는 함수를 감싼다고 해서 이런 이름을 붙였습니다. 다른 이름을 사용해도 상관없습니다).

    def wrapper():                           # 호출할 함수를 감싸는 함수

이제 wrapper 함수에서는 함수의 시작을 알리는 문자열을 출력하고, trace에서 매개변수로 받은 func를 호출합니다. 그다음에 함수의 끝을 알리는 문자열을 출력합니다. 여기서 매개변수로 받은 함수의 원래 이름을 출력할 때는 __name__ 속성을 활용합니다. 마지막으로 wrapper 함수를 다 만들었으면 return을 사용하여 wrapper 함수 자체를 반환합니다.

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # 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

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
@trace    # @데코레이터
def hello():
    print('hello')
 
@trace    # @데코레이터
def world():
    print('world')
 
hello()    # 함수를 그대로 호출
world()    # 함수를 그대로 호출

실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

hello와 world 함수 위에 @trace 를 붙인 뒤에 hello와 world 함수를 그대로 호출하면 끝입니다.

@trace    # @데코레이터
def hello():
    print('hello')
 
@trace    # @데코레이터
def world():
    print('world')
 
hello()    # 함수를 그대로 호출
world()    # 함수를 그대로 호출

물론 다른 함수 위에 @trace를 붙인 뒤 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있습니다.

이 데코레이터를 그림으로 표현하면 다음과 같은 모양이 됩니다.

▼ 그림 42-1 데코레이터

이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있습니다. 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용합니다.

참고 | 데코레이터를 여러 개 지정하기

함수에는 데코레이터를 여러 개 지정할 수 있습니다. 다음과 같이 함수 위에 데코레이터를 여러 줄로 지정해줍니다. 이때 데코레이터가 실행되는 순서는 위에서 아래 순입니다.

@데코레이터1
@데코레이터2
def 함수이름():
    코드

multiple_decorators.py

def decorator1(func):
    def wrapper():
        print('decorator1')
        func()
    return wrapper
 
def decorator2(func):
    def wrapper():
        print('decorator2')
        func()
    return wrapper
 
# 데코레이터를 여러 개 지정
@decorator1
@decorator2
def hello():
    print('hello')
 
hello()

실행 결과

decorator1
decorator2
hello

@을 사용하지 않았을 때는 다음 코드와 동작이 같습니다.

decorated_hello = decorator1(decorator2(hello))
decorated_hello()


https://dojang.io/mod/page/view.php?id=2414



40.3 yield from으로 값을 여러 번 바깥으로 전달하기

지금까지 yield로 값을 한 번씩 바깥으로 전달했습니다. 그래서 값을 여러 번 바깥으로 전달할 때는 for 또는 while 반복문으로 반복하면서 yield를 사용했습니다. 다음은 리스트의 1, 2, 3을 바깥으로 전달합니다.

generate_for_yield.py

def number_generator():
    x = [1, 2, 3]
    for i in x:
        yield i
 
for i in number_generator():
    print(i)

실행 결과

1
2
3

이런 경우에는 매번 반복문을 사용하지 않고, yield from을 사용하면 됩니다. yield from에는 반복 가능한 객체, 이터레이터, 제너레이터 객체를 지정합니다(yield from은 파이썬 3.3 이상부터 사용 가능).

  • yield from 반복가능한객체
  • yield from 이터레이터
  • yield from 제너레이터객체

그럼 yield from에 리스트를 지정해서 숫자 1, 2, 3을 바깥으로 전달해보겠습니다.

generator_yield_from_iterable.py

def number_generator():
    x = [1, 2, 3]
    yield from x    # 리스트에 들어있는 요소를 한 개씩 바깥으로 전달
 
for i in number_generator():
    print(i)

실행 결과

1
2
3

yield from x와 같이 yield from에 리스트(반복 가능한 객체)를 지정했습니다. 이렇게 하면 리스트에 들어있는 요소를 한 개씩 바깥으로 전달합니다. 즉, yield from을 한 번 사용하여 값을 세 번 바깥으로 전달합니다. 따라서 next 함수(__next__ 메서드)를 세 번 호출할 수 있습니다.

>>> g = number_generator()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#105>", line 1, in <module>
    next(g)
StopIteration

40.3.1  yield from에 제너레이터 객체 지정하기

이번에는 yield from에 제너레이터 객체를 지정해보겠습니다(이터레이터는 제너레이터와 동작이 같으므로 생략하겠습니다).

generator_yield_from_generator.py

def number_generator(stop):
    n = 0
    while n < stop:
        yield n
        n += 1
 
def three_generator():
    yield from number_generator(3)    # 숫자를 세 번 바깥으로 전달
 
for i in three_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가 이해되지 않는다고 해서 걱정할 필요가 없습니다. 나중에 프로그래밍에 익숙해지면 자연스럽게 익히게 됩니다.

참고 | 제너레이터 표현식

리스트 표현식을 사용할 때 [ ](대괄호)를 사용했습니다. 같은 리스트 표현식을 ( )(괄호)로 묶으면 제너레이터 표현식이 됩니다. 리스트 표현식은 처음부터 리스트의 요소를 만들어내지만 제너레이터 표현식은 필요할 때 요소를 만들어내므로 메모리를 절약할 수 있습니다.

(식 for 변수 in 반복가능한객체)

>>> [i for i in range(50) if i % 2 == 0]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48]
>>> (i for i in range(50) if i % 2 == 0)
<generator object <genexpr> at 0x024F02A0>



https://dojang.io/mod/page/view.php?id=2413

40.2 제너레이터 만들기

제너레이터와 yield에 대해 알아보았으니 이번에는 range(횟수)처럼 동작을 하는 제너레이터를 만들어보겠습니다.

generator.py

def number_generator(stop):
    n = 0              # 숫자는 0부터 시작
    while n < stop:    # 현재 숫자가 반복을 끝낼 숫자보다 작을 때 반복
        yield n        # 현재 숫자를 바깥으로 전달
        n += 1         # 현재 숫자를 증가시킴
 
for i in number_generator(3):
    print(i)

실행 결과

0
1
2

코드는 간단합니다. 제너레이터 안에서 변수 n을 만들고 0을 저장합니다. 그리고 while n < stop:과 같이 반복을 끝낼 숫자보다 작을 때 반복하도록 만듭니다. 반복문 안에서는 yield n으로 숫자를 바깥으로 전달한 뒤 n을 1 증가시키면 됩니다. 여기서는 yield가 3번 나오므로 for 반복문도 3번 반복합니다.

물론 next 함수(__next__ 메서드)도 3번 사용할 수 있습니다.

>>> g = number_generator(3)
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#100>", line 1, in <module>
    next(g)
StopIteration

40.2.1  yield에서 함수 호출하기

그럼 yield에서 함수(메서드)를 호출하면 어떻게 될까요? 다음은 리스트에 들어있는 문자열을 대문자로 변환하여 함수 바깥으로 전달합니다.

generator_yield_function.py

def upper_generator(x):
    for i in x:
        yield i.upper()    # 함수의 반환값을 바깥으로 전달
 
fruits = ['apple', 'pear', 'grape', 'pineapple', 'orange']
for i in upper_generator(fruits):
    print(i)

실행 결과

APPLE
PEAR
GRAPE
PINEAPPLE
ORANGE

리스트 fruits에 들어있는 문자열이 모두 대문자로 출력되었습니다. yield i.upper()와 같이 yield에서 함수(메서드)를 호출하면 해당 함수의 반환값을 바깥으로 전달합니다. upper는 호출했을 때 대문자로 된 문자열을 반환하므로 yield는 이 문자열을 바깥으로 전달합니다. 즉, yield에 무엇을 지정하든 결과만 바깥으로 전달합니다(함수의 반환값, 식의 결과).

이처럼 yield의 동작 방식만 이해하면 이터레이터보다 훨씬 간단하게 만들 수 있습니다.


https://dojang.io/mod/page/view.php?id=2412



Unit 40. 제너레이터 사용하기

제너레이터는 이터레이터를 생성해주는 함수입니다. 이터레이터는 클래스에 __iter____next__ 또는 __getitem__ 메서드를 구현해야 하지만 제너레이터는 함수 안에서 yield라는 키워드만 사용하면 끝입니다. 그래서 제너레이터는 이터레이터보다 훨씬 간단하게 작성할 수 있습니다.

참고로 제너레이터는 발생자라고 부르기도 합니다. 이 책에서는 제너레이터를 사용하겠습니다.

40.1 제너레이터와 yield 알아보기

함수 안에서 yield를 사용하면 함수는 제너레이터가 되며 yield에는 값(변수)을 지정합니다.

  • yield 

이제 yield를 사용해서 제너레이터를 만들고 for 반복문에서 0, 1, 2 숫자 세 개를 출력해보겠습니다.

yield.py

def number_generator():
    yield 0
    yield 1
    yield 2
 
for i in number_generator():
    print(i)

실행 결과

0
1
2

for 반복문에 number_generator()를 지정해서 값을 출력해보면 yield에 지정했던 0, 1, 2가 나옵니다. 이터레이터와 사용 방법이 똑같죠?

* 커스터마이징 for를 제작해보기https://valuefactory.tistory.com/667?category=765138

40.1.1  제너레이터 객체가 이터레이터인지 확인하기

그럼 number_generator 함수로 만든 객체가 정말 이터레이터인지 살펴보겠습니다. 다음과 같이 dir 함수로 메서드 목록을 확인해봅니다.

>>> g = number_generator()
>>> g
<generator object number_generator at 0x03A190F0>
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

number_generator 함수를 호출하면 제너레이터 객체(generator object)가 반환됩니다. 이 객체를 dir 함수로 살펴보면 이터레이터에서 볼 수 있는 __iter____next__ 메서드가 들어있습니다.

실제로 제너레이터 객체의 __next__를 호출해보면 숫자 0, 1, 2가 나오다가 StopIteration 예외가 발생합니다.

>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
2
>>> g.__next__()
Traceback (most recent call last):
  File "<pyshell#29>", line 1, in <module>
    g.__next__()
StopIteration

이터레이터와 동작이 똑같습니다.

이처럼 함수에 yield만 사용해서 간단하게 이터레이터를 구현할 수 있습니다. 단, 이터레이터는 __next__ 메서드 안에서 직접 return으로 값을 반환했지만 제너레이터는 yield에 지정한 값이 __next__ 메서드(next 함수)의 반환값으로 나옵니다. 또한, 이터레이터는 raise로 StopIteration 예외를 직접 발생시켰지만 제너레이터는 함수의 끝까지 도달하면 StopIteration 예외가 자동으로 발생합니다.

제너레이터는 제너레이터 객체에서 __next__ 메서드를 호출할 때마다 함수 안의 yield까지 코드를 실행하며 yield에서 값을 발생시킵니다(generate). 그래서 이름이 제너레이터(generator)입니다.

40.1.2  for와 제너레이터

그럼 for 반복문과 제너레이터를 살펴보겠습니다. 다음과 같이 for 반복문은 반복할 때마다 __next__를 호출하므로 yield에서 발생시킨 값을 가져옵니다. 그리고 StopIteration 예외가 발생하면 반복을 끝냅니다.

▼ 그림 40-1 for 반복문과 제너레이터

참고로 제너레이터 객체에서 __iter__를 호출하면 self를 반환하므로 같은 객체가 나옵니다(제너레이터 함수 호출 > 제너레이터 객체 > __iter__는 self 반환 > 제너레이터 객체).

그런데 generate라는 키워드를 사용하면 되지 왜 yield라고 이름을 지었을까요? yield는 생산하다라는 뜻과 함께 양보하다라는 뜻도 가지고 있습니다. 즉, yield를 사용하면 값을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보합니다. 따라서 yield는 현재 함수를 잠시 중단하고 함수 바깥의 코드가 실행되도록 만듭니다.

40.1.3  yield의 동작 과정 알아보기

그럼 yield의 동작 과정을 알아보기 위해 for 반복문 대신 next 함수로 __next__ 메서드를 직접 호출해보겠습니다.

  • 변수 = next(제너레이터객체)

yield_next.py

def number_generator():
    yield 0    # 0을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
    yield 1    # 1을 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
    yield 2    # 2를 함수 바깥으로 전달하면서 코드 실행을 함수 바깥에 양보
 
g = number_generator()
 
a = next(g)    # yield를 사용하여 함수 바깥으로 전달한 값은 next의 반환값으로 나옴
print(a)       # 0
 
b = next(g)
print(b)       # 1
 
c = 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)에서 반환된 값을 출력합니다.

▼ 그림 40-2 yield 0의 실행 양보

값을 출력했으면 next(g)로 다시 제너레이터 안의 코드를 실행합니다. 이때는 yield 1이 실행되고 숫자 1을 발생시켜서 바깥으로 전달합니다. 그리고 함수 바깥에서는 print(b)로 next(g)에서 반환된 값을 출력합니다.

▼ 그림 40-3 yield 1의  실행 양보

마찬가지로 과정으로 yield 2도 숫자를 발생시키고 print(c)로 제너레이터에서 나온 값을 출력합니다.

▼ 그림 40-4 yield 2의 실행 양보

이렇게 제너레이터는 함수를 끝내지 않은 상태에서 yield를 사용하여 값을 바깥으로 전달할 수 있습니다. 즉, return은 반환 즉시 함수가 끝나지만 yield는 잠시 함수 바깥의 코드가 실행되도록 양보하여 값을 가져가게 한 뒤 다시 제너레이터 안의 코드를 계속 실행하는 방식입니다. 

참고 | 제너레이터와 return

제너레이터는 함수 끝까지 도달하면 StopIteration 예외가 발생합니다. 마찬가지로 return도 함수를 끝내므로 return을 사용해서 함수 중간에 빠져나오면 StopIteration 예외가 발생합니다.

특히 제너레이터 안에서 return에 반환값을 지정하면 StopIteration 예외의 에러 메시지로 들어갑니다.

generator_return.py

def one_generator():
    yield 1
    return 'return에 지정한 값'
 
try:
    g = one_generator()
    next(g)
    next(g)
except StopIteration as e:
    print(e)    # return에 지정한 값

실행 결과

return에 지


https://dojang.io/mod/page/view.php?id=2366


33.3 클로저 사용하기

이제 함수를 클로저 형태로 만드는 방법을 알아보겠습니다. 다음은 함수 바깥쪽에 있는 지역 변수 ab를 사용하여 a * x + b를 계산하는 함수 mul_add를 만든 뒤에 함수 mul_add 자체를 반환합니다.

closure.py

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산
    return mul_add          # mul_add 함수를 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

먼저 calc에 지역 변수 a와 b를 만들고 3과 5를 저장했습니다. 그다음에 함수 mul_add에서 a와 b를 사용하여 a * x + b를 계산한 뒤 반환합니다.

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산

함수 mul_add를 만든 뒤에는 이 함수를 바로 호출하지 않고 return으로 함수 자체를 반환합니다(함수를 반환할 때는 함수 이름만 반환해야 하며 ( )(괄호)를 붙이면 안 됩니다).

    return mul_add          # mul_add 함수를 반환

이제 클로저를 사용해보겠습니다. 다음과 같이 함수 calc를 호출한 뒤 반환값을 c에 저장합니다. calc에서 mul_add를 반환했으므로 c에는 함수 mul_add가 들어갑니다. 그리고 c에 숫자를 넣어서 호출해보면 a * x + b 계산식에 따라 값이 출력됩니다.

c = calc()
print(c(1), c(2), c(3), c(4), c(5))    # 8 11 14 17 20

잘 보면 함수 calc가 끝났는데도 c는 calc의 지역 변수 ab를 사용해서 계산을 하고 있습니다. 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다. 여기서는 c에 저장된 함수가 클로저입니다.

▼ 그림 33-4 클로저의 개념

이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있습니다. 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용합니다. 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용합니다.

33.3.1  lambda로 클로저 만들기

클로저는 다음과 같이 lambda로도 만들 수 있습니다.

closure_lambda.py

def calc():
    a = 3
    b = 5
    return lambda x: a * x + b    # 람다 표현식을 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

return lambda x: a * x + b처럼 람다 표현식을 만든 뒤 람다 표현식 자체를 반환했습니다. 이렇게 람다를 사용하면 클로저를 좀 더 간단하게 만들 수 있습니다.

보통 클로저는 람다 표현식과 함께 사용하는 경우가 많아 둘을 혼동하기 쉽습니다. 람다는 이름이 없는 익명 함수를 뜻하고, 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 뜻합니다.

33.3.2  클로저의 지역 변수 변경하기

지금까지 클로저의 지역 변수를 가져오기만 했는데, 클로저의 지역 변수를 변경하고 싶다면 nonlocal을 사용하면 됩니다. 다음은 a * x + b의 결과를 함수 calc의 지역 변수 total에 누적합니다.

closure_nonlocal.py

def calc():
    a = 3
    b = 5
    total = 0
    def mul_add(x):
        nonlocal total
        total = total + a * x + b
        print(total)
    return mul_add
 
c = calc()
c(1)
c(2)
c(3)

실행 결과

8
19
33

지금까지 전역 변수, 지역 변수, 변수의 범위, 클로저에 대해 알아보았습니다. 클로저는 다소 어려운 개념이므로 지금 당장 완벽하게 이해하지 않아도 상관없습니다. 나중에 파이썬에 익숙해지면 자연스럽게 익히게 됩니다.


+ Recent posts