-
[Python] 데코레이터 만들기언어/파이썬 & 장고 2021. 8. 8. 19:33
파이썬에서 데코레이터란 @staticmethod와 같은 형태로 클래스나 함수 위에 선언하여 사용하는 것을 말합니다. 데코레이터는 기존 함수 내부의 코드를 수정하지 않고 시작과 끝에 추가 기능을 구현할 수 있습니다.
class Test: @staticmethod def test(): pass
또한 데코레이터는 1개 이상 추가할 수도 있습니다.
from abc import abstractmethod class Test: @staticmethod @abstractmethod def test(): pass
아래에서는 함수와 클래스로 데코레이터를 만드는 방법을 소개합니다.
함수로 데코레이터 만들기
함수로 데코레이터를 만드는 방법은 아래와 같습니다.
def deco1(func): def wrapper(*args, **kwargs): print(func.__name__, 'start') print(f'args: {args}, kwargs: {kwargs}') func(*args, **kwargs) print(func.__name__, 'end') return wrapper @deco1 def test(n, val): print('test 함수') print(n, val) test(10, val=False) # test start # args: (10,), kwargs: {'val': False} # test 함수 # 10 False # test end print(test) # <function deco1.<locals>.wrapper at 0x7f9418035af0>
간단하게는 위와 같이 만들 수 있지만 test 함수를 출력하면 test 함수명이 나오는 것이 아닌 데코레이터의 함수명이 나오게 됩니다. deco1 함수는 그 안에 정의된 wrapper를 반환합니다. 이 wrapper 함수가 데코레이터를 호출한 후 해당 호출을 담고 있는 모듈의 test라는 이름에 할당되는 값입니다. 이러한 동작은 객체 내부를 조사할 때 문제가 될 수 있으므로 functools의 wraps함수를 사용해야 합니다.
from functools import wraps def deco1(func): @wraps(func) def wrapper(*args, **kwargs): print(func.__name__, 'start') print(f'args: {args}, kwargs: {kwargs}') func(*args, **kwargs) print(func.__name__, 'end') return wrapper @deco1 def test(n, val): print('test 함수') print(n, val) test(10, val=False) # test start # args: (10,), kwargs: {'val': False} # test 함수 # 10 False # test end print(test) # <function test at 0x7fa7381cfaf0>
데코레이터의 기능은 그대로 보장하면서 의도한 함수명을 출력하도록 도와줍니다. wraps는 데코레이터를 작성하는 데 이용하는 함수(데코레이터)입니다. 이 함수(데코레이터)를 wrapper 함수에 적용하면 내부 함수에 있는 중요한 메타 데이터가 모두 외부함수로 복사됩니다.
여기서 직접 만든 데코레이터에 변수를 추가하려면 함수를 한 번 더 감싸줘야 합니다.
def deco1(name): def deco1_decorator(func): @wraps(func) def wrapper(*args, **kwargs): print(func.__name__, 'start') print(f'{name}: name') print(f'args: {args}, kwargs: {kwargs}') func(*args, **kwargs) print(func.__name__, 'end') return wrapper return deco1_decorator @deco1(name='이름') def test(n, val): print('test 함수') print(n, val) test(10, val=False) # test start # 이름: name # args: (10,), kwargs: {'val': False} # test 함수 # 10 False # test end
위와 같이 함수를 감싸 3중 함수로 만들면 데코레이터에 파라미터를 전달할 수 있습니다. 위와 같은 형태일 때, @deco1 과 같이 괄호가 빠지게 되면 오류가 발생합니다.
이제 위의 호출 순서를 알아보도록 합니다.
from functools import wraps print(1) def deco1(name='이름'): print(3, name) def deco1_decorator(func): print(5, func) @wraps(func) def wrapper(*args, **kwargs): print(8, args, kwargs) func(*args, **kwargs) print(10) print(6) return wrapper print(4) return deco1_decorator print(2) @deco1() def test(n, val): print(9, 'test') print(7) test(10, val=False) print(11) # 1 # 2 # 3 이름 # 4 # 5 <function test at 0x7fbd000c5af0> # 6 # 7 # 8 (10,) {'val': False} # 9 test # 10 # 11
위의 흐름은 아래와 같이 데코레이터를 사용하지 않았을 때와 동일합니다.
print(1) def deco1(name='이름'): print(3, name) def deco1_decorator(func): print(5, func) def wrapper(*args, **kwargs): print(8, args, kwargs) func(*args, **kwargs) print(10) print(6) return wrapper print(4) return deco1_decorator def test(n, val): print(9, 'test') print(2) deco1_decorator = deco1('이름22') print('#') deco1_call = deco1_decorator(test) print(7) deco1_call(10, val=False) print(11) # 1 # 2 # 3 이름22 # 4 # # # 5 <function test at 0x7fb23800faf0> # 6 # 7 # 8 (10,) {'val': False} # 9 test # 10 # 11
이처럼 만든 데코레이터는 전처리로 유효성 검사나 입력된 파라미터의 값을 변경하고 후처리로 로그를 쌓을 수 있습니다.
from functools import wraps def deco1(func): @wraps(func) def wrapper(*args, **kwargs): name = (args and args[0]) or kwargs.get('name', None) if isinstance(name, str): name = name.upper() else: raise ValueError func(name) log() return wrapper @deco1 def test(name): print(name) test() # KIM
클래스로 데코레이터 만들기
클래스는 함수로 만드는 것 보다 더 간단합니다.
from functools import wraps class Deco1: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print(self.func.__name__, 'start') print(f'args: {args}, kwargs: {kwargs}') self.func(*args, **kwargs) print(self.func.__name__, 'end') @Deco1 def test(name): print(name) test('이름') # test start # args: ('이름',), kwargs: {} # 이름 # test end print(test) # <__main__.Deco1 object at 0x7fc2900a4280>
클래스로 만들면 단점 중에 하나가 함수로 만들었을 때 @wraps를 붙였던 것처럼 위의 코드에선 붙일 수가 없습니다. 데코레이터에 파라미터가 있으면 쉽게 해결이 가능하지만 위처럼 파라미터가 없는 경우, 아래처럼 구현하고 사용해야 합니다.
from functools import update_wrapper class Deco1: def __init__(self, func): self.func = func update_wrapper(self, func) def __call__(self, *args, **kwargs): print(self.func.__name__, 'start') print(f'args: {args}, kwargs: {kwargs}') self.func(*args, **kwargs) print(self.func.__name__, 'end') @Deco1 def test(name): print(name) test('이름') print(test) # <__main__.Deco1 object at 0x7fc2900a4280> print(test.__name__) # test
위 형태를 봤을 땐 파라미터를 받지 않는 데코레이터를 구현할 때엔 함수를 사용하는 것이 더 좋아보입니다.
다음은 파라미터를 받는 데코레이터를 구현한 예제입니다.
from functools import wraps class Deco1: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs def __call__(self, func): @wraps(func) def wrapper(*args, **kwargs): print(func.__name__, 'start') print(self.args, self.kwargs) print(f'args: {args}, kwargs: {kwargs}') func(*args, **kwargs) print(func.__name__, 'end') return wrapper @Deco1("파라미터", asid=123) def test(name): print(name) test('이름') # test start # ('파라미터',) {'asid': 123} # args: ('이름',), kwargs: {} # 이름 # test end print(test) # <function test at 0x7f8e100f2040>
요약
파라미터가 없는 데코레이터를 만들 땐, 클래스보단 함수로 진행하는 것이 좋다.
데코레이터 생성 후, @wraps는 필수로 붙여주자