본문 바로가기

진리는어디에/Python

[Python] 데코레이터(Decorator) #4 활용

이번 강좌는 데코레이터 시리즈의 마지막 강좌로써 Decorator #1Decorator #2, Decorator #3와 이어지는 내용이다. 지난 강좌를 보지 않으면 이해하기 어려운 부분이 있으므로 꼭 이전 강좌들을 먼저 볼것을 추천한다.

목차

  1. 파이썬 데코레이터 소개
  2. 함수의 인자와 리턴값 처리하기
  3. 인자를 가지는 데코레이터
  4. >> 데코레이터 활용

데코레이터 활용

이번 시간에는 지금 까지 배운것을 토대로 객체에 함수를 등록해주는 register라는 데코레이터를 만들어 보도록 하겠다. 종종 유용하게 쓰이는 패턴이므로 주의 깊게 살펴 보도록 하자.

먼저 데코레이터가 적용 되지 않은 코드를 먼저 살펴보자. 

# 객체(obj)에 함수(func)를 등록하는 함수
def register(obj, func) :
    setattr(obj, func.__name__, func)
    
def inner() :
    pass
    
def some_func() :
    print('some_func')
    
# inner 함수 객체에 some_func 함수 등록
register(inner, some_func)

inner.some_func() # some_func
setattr(object, attribute_name, value)

객체에 존재하는 어트리뷰트를 변경하거나 새로운 어트리뷰트를 생성하여 값을 부여한다.
"setattr(obj, 'new_attr', value)", "obj.new_attr = value", "obj.__dict__['new_attr'] = value" 는 모두 같은 의미다.

register 함수를 이용해 inner 함수 객체에 some_func함수를 어트리뷰트로 추가하는 코드다. 그런데 이걸 좀 더 간단하고 깔끔하게 아래 처럼 데코레이터를 이용할 수는 없을까?

@register(inner) # some_func함수를 inner객체의 어트리뷰트로 등록한다
def some_func() :
    print('some_func')

일단 위와 같이 some_func에 데코레이터를 선언 후 실행하면, "TypeError: register() missing 1 required positional argument: 'func'" 라는, 인자가 하나 부족하다는 예외를 발생 시킨다. 

그러면 좀 아름답지 않긴 하지만 아래 처럼 데코레이터에 인자를 두개 사용하여 넘겨주는 방법도 있지 않을까 생각 할 수 있다.

@register(inner, some_func)  # 인자를 register 함수와 맞추어 주었다
def some_func() :
    print('some_func')

이미 예상하신 분들도 있겠지만 당연히 안된다. 데코레이터의 인자 개수 문제가 아니라, 데코레이터를 호출하는 시점에는 some_func함수가 정의되어 있지 않기 때문에 'NameError: name 'some_func' is not defined' 오류를 발생 시킨다. 우리는 좋든 싫든 인터프리터가 넘겨주는 함수 객체를 이용 할 수 밖에 없다.

문제 상황
꾸며줘야 할 함수를 데코레이터의 인자로 받아야 한다

다시 본론으로 돌아가, 이전 장에서 데코레이터를 선언하면 인터프리터가 아래와 같이 코드를 변경한다고 이야기 했었다. 

# @register(inner)
# def some_func() :
#    print('some_func')

some_func = register(inner)(some_func)

여기서 문제가 발생한다. 인터프리터가 만들어주는 함수의 형태는 some_func = register(inner)(some_func)지만 register 함수를 사용하려면some_func = register(inner, some_func)와 같이 호출해야 한다. 함수의 인자가 부족하다는 에러는 이 때문이었다.

그래서 이번 포스트에서는, register(obj, func)와 같은 형식을 register(inner)(some_func)와 같이 호출 할 수 있는 방법에 대해 알아 보고자 한다.

커링(Currying)

결국 우리가 하고 싶은 것은 인자가 두개인 함수를, 인자가 한개인 함수들의 연속 호출로 바꾸는 것이다. 이런 기술을 커링(Currying)이라고 한다.

예를 들어 인자가 2개인 함수를 인자가 1개인 함수로 2번 연속 호출한다던지, 인자가 5개인 함수를 2개, 3개씩 연속으로 호출하는 것을 말한다. 중요한 것은 인자의 개수와 호출 회수가 아니라, 인자가 여러개인 함수를 한번 호출하는 대신, 인자들을 여러개로 쪼개어 연속으로 함수 호출을 여러번 하는 것이다.

커링(Currying)
인자가 여러개인 함수를 한번 호출하는 대신, 인자들을 여러개로 쪼개어 연속으로 호출 하는 기법

첫번째, 방법으로는 Decorator #3 강좌에서 다루었던것 처럼 함수에서 내부 함수를 계속 리턴하는 방법이 있다. 

def register(obj) :

    def decorator(func) :
        nonlocal obj
        setattr(obj, func.__name__, func)
        
        def inner(*args, **kwargs) :
            return func(*args, **kwargs)
        
        return inner
    return decorator

register(obj, func)의 인자리스틀 register(obj) 처럼 변경하고, register의 리턴 값으로 다음 인자, 즉, func를 받을 수 있는 decorator 함수를 리턴했다. 풀어서 적어보면 아래와 같다.

# some_func = register(inner)(some_func)

decorator = register(inner)      # decorator 내부 함수 객체 리턴
some_func = decorator(some_func) # inner 내부 함수 객체 리턴

이제 다시 가장 처음 나왔던 예제에 데코레이터를 적용해보도록 하자. register 함수를 선언하면서 inner 함수의 어트리뷰트로 정상 등록 된 것을 확인 할 수 있다.

def inner() :
    pass
    
@register(inner)
def some_func() :
    print('some_func')
    
# 기존 register 호출 코드
# register(inner, some_func) 

inner.some_func() # some_func

functools.partial()

위 내부 함수를 리턴하는 커링 방법도 동작하긴 하지만 코드 복잡도가 올라가고, 그만큼 어려운 코드가 되었다(함수가 3중이나 중첩이 필요하다!!). 이럴 때 유용하게 사용 할 수 있는 것이 functoos 모듈의 partial 함수다. 

partial 함수란, 인자로 넘겨지는 기존 함수와 동일하지만, 파라미터를 미리 정해준 또 다른 함수를 리턴하는 함수다. 자세한 사항은 [여기]를 참고 하도록 한다.

이번에는 partial 함수를 이용하여 보다 간단하게 위의 예제와 동일한 커링을 구현 해보도록 하자. 먼저 register의 func 인자에 디폴트 인자를 지정하여 인자 하나만으로도 호출이 가능 할 수 있게 해보자.

# def register(obj, func)
def register(obj, func=None)

그리고 다음 인자를 받는 연속된 호출이 되어야 하므로 register에서 함수를 리턴해보도록 하자. 하지만 이번에는 따로 함수를 정의하는 것이 아닌 partial 함수를 이용해 첫번째 인자만 미리 지정한 register함수를 리턴하자.

from functools import partial

def register(obj, func=None) :
    if func is None :
        return partial(register, obj) # register를 리턴할 것인데 첫번째 인자를 obj로 고정
        
    setattr(obj, func.__name__, func)
    return func
    
# @register(inner)
# def some_func() :
#    print('some_func')
#
# some_func = register(inner)(some_func)
partial_obj = register(inner)      # register와 동일하지만 첫번째 인자를 obj로 지정한 register 리턴
some_func = partial_obj(some_func) # obj는 위에서 지정 되었으므로 some_func만 필요

이전에 작성했던 코드 보다 함수의 뎁스도 줄어 들고 코드도 간결해졌다. 

그럼 이 register를 우리가 지금까지 다듬어 왔던 add_emoticon에 적용해 보도록 하자. add_emoticon에 change_emoticon 함수를 추가하여, 데코레이터 결과로 리턴되는 inner 함수 객체의 새로운 어트리뷰트로 추가 할 예정이다.

from functools import wraps
from functools import partial

# 객체(obj)에 함수(func)를 등록하는 함수
def register(obj, func=None) :
    if func is None :
        return partial(register, obj) # register를 리턴할 것인데 첫번째 인자를 obj로 고정
        
    setattr(obj, func.__name__, func)
    return func

def add_emoticon(emoticon='^_^') :

    def decorator(func) :
    
        @wraps(func)
        def inner(*args, **kwargs) :
            # nonlocal func
            print(emoticon, ' ', end='')
            result = func(*args, **kwargs)
            return result
            
        @register(inner)
        def change_emoticon(e) :
            nonlocal emoticon
            emoticon = e
            
        return inner
                
    return decorator    

@add_emoticon('$_$')
def say_hello(name) :
    print(f'hello {name}')
    
say_hello('jason')                # $_$  hello jason
say_hello.change_emoticon('-_-')
say_hello('jason')                # -_-  hello jason

23라인에서 register 데코레이터를 이용하여 inner 함수에 change_emoticon을 새로운 어트리뷰트로 등록하고 있다.

@register(obj) 데코레이터는

  • 함수에 기능을 추가하기 위한 목표가 아니라 "함수를 객체에 등록"하기 위한 데코레이터
  • register 함수 자체는 "최초에 한번만 호출" 된다는 특징을 활용한 기술

마치며

이상으로 기존 함수에 기능을 추가하기 위해 사용되는 용도의 데코레이터 외에도 단순 특정 로직을 실행하기 위한 데코레이터도 있음을 살펴 보았다. 특히 위에서 배운 여러개인 인자를 한번에 받는 대신 하나의 인자를 여러번에 받는 커링이라고하는 기술은 자주 사용되는 기술이므로 주의 깊게 보도록 하자.

이상으로 데코레이터에 관련된 모든 강좌가 끝났다. 다음 강좌에서는 파이썬 클래스(class)에 대해 알아 보도록 하겠다.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!