본문 바로가기

진리는어디에/Python

[Python] 데코레이터(Decorator) #2 함수의 인자와 리턴값 처리

이번 포스트는 인자와 리턴 값을 갖는 함수에 데코레이터를 적용하는 방법에 대해 알아 본다. 또한 함수를 데코레이팅함으로써 발생하는 함수의 메타 정보에 대한 문제들과 그 해결 방법에 대해 집중적으로 다루도록 하겠다.

목차

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

함수 인자와 리턴값 처리하기

[이전 강의]의 예제에서는 인자와 리턴 값이 없는 함수를 대상으로한 데코레이터 예제를 살펴 보았다. 그렇다면 아래와 같이 say_hello 함수에 인자가 추가 되면 어떻게 될까?

def add_emoticon(func) :

    def inner() :
         print('= ̄ω ̄= ', end='')
         func()
    
    return inner
    
@add_emoticon
def say_hello(name) :
    print(f'hello {name}!')

say_hello('james') # TypeError: inner() takes 0 positional arguments but 1 was given

위 예제를 실행 시키면, 데코레이터 함수로 불리는 inner함수는 0개의 인자를 기대하는데, 1개의 인자가 주어졌다는 TypeError를 발생 시킨다. 이는 say_hello() 함수의 name 인자가, 데코레이터 함수 호출 시 그대로 전달 되기 때문이다.

그럼 이 문제를 해결하기 위해 inner 함수가 say_hello가 넘겨주는 name 파라미터를 처리 할 수 있도록 코드를 수정해 보자.

def add_emoticon(func) :

    def inner(name) :
         print('= ̄ω ̄= ', end='')
         func(name)
    
    return inner
    
@add_emoticon
def say_hello(name) :
    print(f'hello {name}!')

say_hello('james') # = ̄ω ̄= hello james!

inner 함수는 인터프리터로 부터 넘어온 name 파라미터를 func(=say_hello)에게 잘 전달하여 우리가 원하는 결과를 출력 했다.

그럼 이제 과연 아무런 문제가 없을까?

진짜 다 된거 맞을까?

say_hello의 name에 인자에 대한 처리는 했지만 아무런 인자가 없는 say_hi 함수에 대한 처리는 어떻게 해야 할까? 만일 인자가 세개이거나 네개인 함수가 있다면? 모든 다른 인자 개수를 가지고 있는 함수마다 똑같은 이모티콘을 먼저 출력해주는 데코레이터들을 만들어야 할까?

다행히 우리는 파라미터 패킹 / 언패킹 포스트에서 함수의 인자가 몇개든 상관 없이 처리 할 수 있는 '가변 인자 함수'에 대해 배웠다.

파라미터 패킹과 언패킹에 간단히 설명하면, 패킹은 여러 개의 함수의 인자들을 하나의 튜플(tuple) 또는 딕셔너리(dict) 객체로 묶는 것을 말하고, 언패킹은 튜플(tuple) 또는 딕셔너리(dict) 객체의 각 요소들을 함수의 개별 인자로 풀어서 호출하는 것을 말한다. 자세한 사항은 파라미터 패킹언패킹 포스트를 참고 하도록 한다.

inner 함수를 인자의 개수와 상관 없이 처리 할 수 있는 가변 인자 함수로 변경해 보도록 하겠다.

def add_emoticon(f) :

    def inner(*args, **kwargs) :    # 파라미터 패킹 - https://kukuta.tistory.com/318
        print('= ̄ω ̄= ', end='')
        result = f(*args, **kwargs) # 파라미터 언패킹 - https://kukuta.tistory.com/317
        return result
        
    return inner
    
@add_emoticon
def say_hello(name) :
    print(f'hello {name}!')

@add_emoticon
def say_hi() :
    print('hi')
    
say_hello('james') # ok, = ̄ω ̄= hello james!
say_hi()           # ok, = ̄ω ̄= hi

함수의 인자 리스트에 * 또는 **를 사용하여 inner함수로 넘어오는 인자가 몇개든 튜플과 딕셔너리 객체로 넘겨 받을 수 있도록 하고, inner에서 원본함수를 호출 할 때는 언패킹을 이용하여 args와 kwargs의 각 요소를 함수의 개별 인자로 풀어서 전달 하도록 했다. 추가로 현재 say_hello, say_hi는 반환 값이 없지만 원본 함수로 부터 반환 값이 있다면 반환 값도 그대로 전달 할 수 있도록 수정 했다.

이제는 say_hello() 함수와 say_hi() 함수의 인자가 다르더라도 동일한 데코레이터를 사용하여 이모티콘을 출력하고 있는 것을 볼 수 있다. 데코레이터는 자신이 할 일(이모티콘 출력하기)만 하고, 넘겨지는 함수의 인자가 무엇인지에 상관 없이 그대로 원래 함수(say_hello, say_hi)에게 전달한다. 이것을 Perfect-forwarding이라고 한다.

함수 메타 데이터 처리하기

이전 강의 함수 객체에서 함수 객체가 가지고 있는 어트리뷰트들에 대해 알아 보았었다. 어트리뷰트들은 함수의 이름, 설명, 기본 파라미터들과 같은 메타정보를 저장하는데 사용된다. 어트리뷰트에 관한 자세한 설명은 [여기]를 참고 하도록 한다.

say_hello 함수에 아래와 같이 함수의 도입부에 주석을 추가하면 __doc__어트리뷰트에 함수의 설명이 저장된다. 

def say_hello(name) :
    '''this is say_hello function'''
    print(f'hello {name}!')
    
print(say_hello.__name__)  # say_hello, 함수의 이름 출력
print(say_hello.__doc__)   # this is say_hello function, 함수의 설명 문서 출력

일단 @add_emoticon을 빼고 실행 시켜 보면 함수의 이름과 설명이 정상적으로 출력 되는 것을 볼 수 있다.

하지만 여기에 @add_emoticon 데코레이터 적용하는 순간 문제가 된다. 앞에서 데코레이터를 적용하면 결국 우리가 호출 하는 say_hello함수는 add_emoticon 함수 안에 선언된 inner 함수라고 이야기 했었다. 그럼 데코레이터를 적용하는 순간 결과는 어떻게 되겠는가? say_hello.__name__은 inner가 되고, say_hello.__doc__는 (지금은 아무것도 없지만) inner함수의 설명이 될 것이다.

@add_emoticon
def say_hello(name) :
    '''this is say_hello function'''
    print(f'hello {name}!')
    
print(say_hello.__name__)  # inner
print(say_hello.__doc__)   # None

위와 같이 데코레이터를 사용하면 함수의 메타데이터들이 망가져버린다. 이 문제를 해결하기 위해 파이썬에서는 functool 모듈의 wrap를 제공한다. 사용법은 아래와 같이 inner함수 앞에 wraps 데코레이터를 추가 해주면 된다.

from functools import wraps

def add_emoticon(f) :

    @wraps(f)
    def inner(*args, **kwargs) :
         print('= ̄ω ̄= ', end='')
         result = f(*args, **kwargs)
         return result
    return inner
    
@add_emoticon
def say_hello(name) :
    '''this is say_hello function'''
    print(f'hello {name}!')
    
print(say_hello.__name__)  # say_hello
print(say_hello.__doc__)   # this is say_hello function

여기서 뭔가 이상함을 느낀 분들도 있을 것이다. 우리가 지금까지 작성했던 데코레이터들은 괄호가 없었는데 wraps는  그와는 다르게 괄호도 있고 인자도 받는다. 이 부분에 대한 설명은 다음 포스트에서 이어 하도록 하겠다. 지금은 일단 @wraps를 적용하면 데코레이터 대상 함수의 메타정보를 자신이 대신 저장하고 있다가 요청시 대신 출력해준다는 정도만 기억하고 넘어 가도록 하자.

마치며

지금까지 데코레이터의 기본 사용법, 함수와 리턴 값의 처리, 메타데이터 관련 문제에 대해 알아 보았다. 앞에서 언급된 내용들을 정리하면 아래와 같은 데코레이터의 기본 포멧을 만들어 낼 수 있다.

from functools import wraps

def 데코레이터_이름(f) :

    # 데코레이터 초기화 영역

    @wraps(f)
    def 내부함수_이름(*args, **kwargs) :
         # 대상 함수 호출 전 작업
         result = f(*args, **kwargs) # f는 원본 함수 객체
         # 대상 하수 호출 후 작업
         return result
    
    return 내부함수_이름
    
@데코레이터_이름
def 대상함수() :
    함수내용

데코레이터가 '@데코레이터_이름'과 같이 함수에 적용 되면 주석으로 표시된 '데코레이터 초기화 영역'이 한 번 실행 된다. 그리고 함수가 호출 될 때 마다 '대상 함수 호출 전/후 작업'이 호출 된다.

위 앞으로 데코레이터를 이용해 기존에 존재하는 함수에 기능을 추가하게 된다면 위 기본 형태의 데코레이터를 이용해 적절하게 필요한 내용들을 추가 해주면 된다.

다음 강좌 Decorator #3에서는 위에서 살짝 언급하고 넘어 갔던 인자를 받을 수 있는 데코레이터에 대해 살펴 보도록 하겠다.

부록 1. 같이 보면 좋은 글

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