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