python 데커레이터!

데커레이터 (decorator)

파이썬 2.2 도입 다른 함수를 인수로 받아 새롭게 수정된 함수로 대체하는 함수 주로 권한검사, 로깅 등의 함수안에서 고정된, 반복되는 코드를 작성해야할 때 자주 사용한다.

def identity(func):
    print("hello")
    return func

@identity
def foo():
    return 'bar'

만약 위와같이 함수정의가 되어있다면 아래와 같은 코드로 변경된다.

def foo():
  return 'bar'

foo = identity(foo)

초기 foo() 함수는 그대로 있고 identity() 함수를 겨쳐 foo() 가 재정의된다.
실제 우리가 정의했던 foo() 함수는 재정의된 함수 별도의 공간에 참조변수로서 존재하게 된다.

wrapper 형태

일반적으로 정의한 함수의 매개변수 타입, 개수 모두 다르기때문에 데커레이터 함수 정의시 wrapper함수 를 사용하는게 대부분

def check_is_admin(func):
    def wrapper(*args, **kwargs):
        if kwargs.get('username') != 'admin':
            raise Exception("username is not admin, name:%s" % kwargs.get('username'))
        return func(*args, **kwargs)

    return wrapper

@check_is_admin
def test_func():
    print("hello")

if __name__ == '__main__':
    test_func('hello', 'world', username="kouzie", age=30)
    # Exception: username is not admin, name:kouzie

test_func 함수는 현재 wrapper 로 재정의된 클래스로 매개변수를 위처럼 딕셔너리형태로 전달해도 kwargs 가 받아 정상동작되고 Exception 이 출력된다.

물론 진짜 우리가 정의한 내부의 참조변수인 func 를 호출하게될 경우 매개변수가 없는 함수를 호출했다고 오류가 발생한다.

다중 데커레이터, 데커레이터 매개변수

데커레이터 자체에 매개변수 전달이 가능하다.
또한 여러번 데커레이터를 적용할 경우 메서드 재정의가 한번 더 이루어지면서 2번 depth 데커레이터를 통해 원본 함수를 호출하게된다.

def check_is_admin_mapping(name):
    def check_is_admin(func):
        def wrapper(*args, **kwargs):
            if kwargs.get('username') != name:
                raise Exception("username is not {0}, name:{1}".format(name, kwargs.get("username")))
            else:
                print("correct user", kwargs.get("username"))
                return func(*args, **kwargs)
        return wrapper
    return check_is_admin

@check_is_admin_mapping("kouzie")
@check_is_admin_mapping("test")
def test_func():
    print("hello")

if __name__ == '__main__':
    test_func(username="kouzie", age=30)
    # correct user kouzie , 첫번째 데커레이터 진행 성공
    # Exception: username is not test, name:kouzie, 두번째 데커레이터 실패

처음에는 correct user 문자열을 호출했지만 두번째 데커레이터 호출에서는 에러를 반환한다.

변환되는 과정을 코드로 나타내면 아래와 같다.
이미 재정의된 함수를 다시 재정의하기 때문에 이중으로 재정의되어 위와같이 실행된다.

test_func = check_is_admin_mapping("kouzie")(test_func)
test_func = check_is_admin_mapping("test")(test_func)

클래스와 데커레이터

먼저 클래스 위에 데커레이터를 사용해 클래스를 조작하는 방법이 있다.

import uuid

def get_name_id(self):
    return str(self.name) + str(self.uuid)

def set_class_name_and_id(klass):
    klass.name = str(klass)
    klass.uuid = uuid.uuid4()
    klass.get_name_id = get_name_id
    return klass

@set_class_name_and_id
class SomeClass(object):
    pass

if __name__ == '__main__':
    sc = SomeClass()
    print(SomeClass.name)  # <class '__main__.SomeClass'>
    print(sc.uuid)  # 4dff7a95-a3cd-4930-a7a9-dac385b8c55b
    print(sc.get_name_id())  # <class '__main__.SomeClass'>2ad3944d-5bdc-4ac6-938d-a94c3390339a


클래스변수, 클래스함수를 데커레이터를 통해 추가가능하다

함수위에 데커레이터를 사용해 함수를 클래스화 하는 방법도 있다(클래스 데커레이터) 함수 혹은 클래스를 클래스로 래핑하는 역할

class CountCalls(object):
    def __init__(self, f):
        self.f = f
        self.called = 0

    def __call__(self, *args, **kwargs):
        self.called += 1
        return self.f(*args, **kwargs)

@CountCalls
def print_hello():
    print("hello")

아래와 같이 함수가 클래스로 재정의되어 클래스의 __call__ 함수를 호출하게되는 형태로 변환

print_hello = CountCalls(print_hello)

update_wrapper

def foobar():
    """foo~~bar~~"""
    print("goo")

if __name__ == '__main__':
    print(foobar.__doc__)  # foo~~bar~~
    print(foobar.__name__)  # foobar
def test_deco(f):
    def wrapper(*args, **kwargs):
        print("hello")
        return f(*args, **kwargs)
    return wrapper

@test_deco
def foobar():
    """foo~~bar~~"""
    print("boo")

if __name__ == '__main__':
    print(foobar.__doc__)  # None
    print(foobar.__name__)  # foobar

한번 감싸는 순간 함수가 재정의 되기때문에 기존 함수의 메타데이터들(속성, 이름) 들을 잃어버린다.
파이썬 자동 문서화등에서 문제가 발생하수 있기때문에 원복해주는 functools 패키지의 update_wrapper() 함수를 사용하는 것을 권장

import functools

def test_deco(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    wrapper = functools.update_wrapper(wrapper, f)
    return wrapper

@test_deco
def foobar():
    """foo~~bar~~"""
    print("goo")


if __name__ == '__main__':
    print(foobar.__doc__)  # foo~~bar~~
    print(foobar.__name__)  # foobar

update_wrapper 내부 코드를 보면 '__module__', '__name__', '__qualname__', '__doc__', '__annotations__' 정보를 내부 별도 변수에 담아두었다가 래퍼 함수 참조변수에 할당하도록 되어있다.

한줄 추가하는것 간단한 일이지만 좀더 좋은 직관성, 짧은 코드를 위해 update_wrapper 기능을 하는 데커레이터용 함수가 정의되어 있다.

import functools

def test_deco(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

@test_deco
def foobar():
    """foo~~bar~~"""
    print("goo")


if __name__ == '__main__':
    print(foobar.__doc__)  # foo~~bar~~
    print(foobar.__name__)  # foobar