30. 데코레이터(Decorator)
1. 키워드
- 데코레이터(Decorator)
- 클로저(Closure)
2. 데코레이터 사용하기
- 지금까지 클래스에서 메서드를 만들 때
@staticmethod
,@classmethod
,@abstractmethod
등을 붙였는데, 이렇게@
로 시작하는 것들이 데코레이터이다.
- 즉, 다음과 같이 함수(메서드)를 장식한다고 해서 이런 이름이 붙었다.
3. 데코레이터 만들기
- 데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용한다.
- 예를 들어 함수의 시작과 끝을 출력하고 싶다면 다음과 같이 함수 시작, 끝 부분에
print
를 넣어주어야 한다.
def hello():
print("hello 함수 시작")
print("hello")
print("hello 함수 끝")
def world():
print("world 함수 시작")
print("world")
print("world 함수 끝")
hello()
world()
# hello 함수 시작
# hello
# hello 함수 끝
# world 함수 시작
# world
# world 함수 끝
- 만약 다른 함수도 시작과 끝을 출력하고 싶다면 함수를 만들 때마다
print
를 넣어야 한다. - 따라서 함수가 많아지면 매우 번거로워지기 때문에 이런 경우에는 데코레이터를 활용하면 편리하다.
- 다음은 함수의 시작과 끝을 출력하는 데코레이터이다.
def trace(func):
def wrapper():
print(func.__name__, "함수 시작")
func()
print(func.__name__, "함수 끝")
return wrapper
def hello():
print("hello")
def world():
print("world")
trace_hello = trace(hello)
trace_hello()
trace_world = trace(world)
trace_world()
# hello 함수 시작
# hello
# hello 함수 끝
# world 함수 시작
# world
# world 함수 끝
hello
와world
함수의 시작과 끝이 출력되었다.
- 먼저 데코레이터
trace
는 다음과 같이 호출할 함수를 매개변수로 받는다. trace
는 추적하다라는 뜻인데, 프로그래밍에서 함수의 실행 상황을 추적할 때trace
라는 말을 사용한다.
trace
함수 안에서는 호출할 함수를 감싸는 함수wrapper
를 만든다.
- 이제
wrapper
함수에서는 함수의 시작을 알리는 문자열을 출력하고,trace
에서 매개변수로 받은func
를 호출한다. - 그다음에 함수의 끝을 알리는 문자열을 출력한다.
- 여기서 매개변수로 받은 함수의 원래 이름을 출력할 때는
__name__
속성을 활용한다. - 마지막으로
wrapper
함수를 다 만들었으면return
을 사용하여wrapper
함수 자체를 반환한다.
def trace(func):
def wrapper():
print(func.__name__, "함수 시작")
func()
print(func.__name__, "함수 끝")
return wrapper
- 즉, 함수 안에서 함수를 만들고 반환하는 클로저이다.
- 데코레이터를 사용할 때는 다음과 같이
trace
에 호출할 함수hello
또는world
를 넣는다. - 그다음에 데코레이터에서 반환된 함수를 호출한다.
- 이렇게 하면 데코레이터에 인수로 넣은 함수를 호출하고 시작과 끝을 출력한다.
- 물론
trace
에 다른 함수를 넣은 뒤 반환된 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있다.
1) @
로 데코레이터 사용하기
- 이제
@
을 사용하여 좀 더 간편하게 데코레이터를 사용해 보자.
- 다음과 같이 호출할 함수 위에
@데코레이터
형식으로 지정한다.
def trace(func):
def wrapper():
print(func.__name__, "함수 시작")
func()
print(func.__name__, "함수 끝")
return wrapper
@trace
def hello():
print("hello")
@trace
def world():
print("world")
hello()
world()
# hello 함수 시작
# hello
# hello 함수 끝
# world 함수 시작
# world
# world 함수 끝
hello
와world
함수 위에@trace
를 붙인 뒤에hello
와world
함수를 그대로 호출하면 끝이다.
- 물론 다른 함수 위에
@trace
를 붙인 뒤 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있다.
- 이 데코레이터를 그림으로 표현하면 다음과 같은 모양이 된다.
- 이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있다.
- 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용한다.
데코레이터를 여러 개 지정하기
- 함수에는 데코레이터를 여러 개 지정할 수 있다.
- 다음과 같이 함수 위에 데이터를 여러 줄로 지정해 주는데, 이때 데코레이터가 실행되는 순서는 위에서 아래 순이다.
def decorator1(func):
def wrapper():
print("decorator1")
func()
return wrapper
def decorator2(func):
def wrapper():
print("decorator2")
func()
return wrapper
@decorator1
@decorator2
def hello():
print("hello")
hello()
# decorator1
# decorator2
# hello
@
을 사용하지 않았을 때는 다음 코드와 동작이 같다.
4. 매개변수와 반환값을 처리하는 데코레이터 만들기
- 지금까지 매개변수와 반환값이 없는 함수의 데코레이터를 만들었다.
- 이번에는 매개변수와 반환값을 처리하는 데코레이터는 어떻게 만드는지 알아보자.
- 다음은 함수의 매개변수와 반환값을 출력하는 데코레이터이다.
def trace(func):
def wrapper(a, b):
r = func(a, b)
print(f"{func.__name__}(a={a}, b={b}) -> {r}")
return r
return wrapper
@trace
def add(a, b):
return a + b
print(add(10, 20))
# add(a=10, b=20) -> 30
# 30
add
함수를 호출했을 때 데코레이터를 통해서 매개변수와 반환값이 출력되었다.
- 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 다음과 같이 먼저 안쪽
wrapper
함수의 매개변수를 호출할 함수add(a, b)
의 매개변수와 똑같이 만들어준다.
wrapper
함수 안에서는func
를 호출하고 반환값을 변수에 저장한다.- 그다음에
print
로 매개변수와 반환값을 출력한다. - 이때
func
에는 매개변수a
와b
를 그대로 넣어준다. - 또한,
add
함수는 두 수를 더해서 반환해야 하므로func
의 반환값을return
으로 반환해 준다.
def trace(func):
def wrapper(a, b):
r = func(a, b)
print(f"{func.__name__}(a={a}, b={b}) -> {r}")
return r
return wrapper
- 만약
wrapper
함수에서func
의 반환값을 반환하지 않으면add
함수를 호출해도 반환값이 나오지 않으므로 주의해야 한다. - 참고로
wrapper
함수에서func
의 반환값을 출력할 필요가 없으면return func(a, b)
처럼func
를 호출하면서 바로 반환해도 된다.
- 데코레이터를 사용할 때는
@
로 함수 위에 지정해 주면 된다. - 또한,
@
로 데코레이터를 사용했으므로add
함수는 그대로 호출해 준다.
1) 가변 인수 함수 데코레이터
def add(a, b):
는 매개변수의 개수가 고정된 함수이다.
- 매개변수(인수)가 고정되지 않은 함수를 처리할 때는 다음과 같이
wrapper
함수를 가변 인수 함수로 만들면 된다.
def trace(func):
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
print(f"{func.__name__}(args={args}, kwargs={kwargs}) -> {r}")
return r
return wrapper
@trace
def get_max(*args):
return max(args)
@trace
def get_min(**kwargs):
return min(kwargs.values())
print(get_max(10, 20))
print(get_min(x=10, y=20, z=30))
# get_max(args=(10, 20), kwargs={}) -> 20
# 20
# get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
# 10
get_max
함수와get_min
함수는 가변 인수 함수이다.- 따라서 데코레이터도 가변 인수 함수로 만들어준다.
- 이때 위치 인수와 키워드 인수를 모두 받을 수 있도록
*args
와**kwargs
를 지정해 준다.
- 이렇게 만든 데코레이터
trace
는 위치 인수와 키워드 인수를 모두 처리할 수 있다. - 따라서 가변 인수 함수뿐만 아니라 일반적인 함수에도 사용할 수 있다.
메서드에 데코레이터 사용하기
- 클래스를 만들면서 메서드에 데코레이터를 사용할 때는
self
를 주의해야 한다. - 인스턴스 메서드는 항상
self
를 받으므로 데코레이터를 만들 때도wrapper
함수의 첫 번째 매개변수는self
로 지정해야 하고, 반대로 클래스 메서드라면cls
로 지정해야 한다. - 마찬가지로
func
를 호출할 때도self
와 매개변수를 그대로 넣어야 한다.
5. 매개변수가 있는 데코레이터 만들기
- 이번에는 매개변수가 있는 데코레이터를 만들어보자.
- 이런 방식의 데코레이터는 값을 지정해서 동작을 바꿀 수 있다.
- 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터이다.
def is_multiple(x):
def real_decorator(func):
def wrapper(a, b):
r = func(a, b)
if r % x == 0:
print(f"{func.__name__}의 반환값은 {x}의 배수입니다.")
else:
print(f"{func.__name__}의 반환값은 {x}의 배수가 아닙니다.")
return r
return wrapper
return real_decorator
@is_multiple(3)
def add(a, b):
return a + b
print(add(10, 20))
print(add(2, 5))
# add의 반환값은 3의 배수입니다.
# 30
# add의 반환값은 3의 배수가 아닙니다.
# 7
- 실행을 해보면
add
함수의 반환값이3
의 배수인지 아닌지 알려준다.
- 지금까지 데코레이터를 만들 때 함수 안에 함수를 하나만 만들었지만, 매개변수가 있는 데코레이터를 만들 때는 다음과 같이 함수를 하나 더 만들어야 한다.
- 먼저
is_multiple
함수를 만들고 데코레이터가 사용할 매개변수x
를 지정한다. - 그리고
is_multiple
함수 안에서 실제 데코레이터 역할을 하는real_decorator
를 만든다. - 즉, 이 함수에서 호출할 함수를 매개변수로 받은 다음에
real_decorator
함수 안에서wrapper
함수를 만들어주면 된다.
wrapper
함수 안에서는 먼저func
의 결과가 데코레이터 매개변수x
의 배수인지 확인한다.- 그다음에
func
의 반환값을 반환한다.
def is_multiple(x):
def real_decorator(func):
def wrapper(a, b):
r = func(a, b)
if r % x == 0:
print(f"{func.__name__}의 반환값은 {x}의 배수입니다.")
else:
print(f"{fumc.__name__}의 반환값은 {x}의 배수가 아닙니다.")
return r
- 여기서는
real_decorator
,wrapper
함수를 두 개 만들었으므로 함수를 만든 뒤에return
으로 두 함수를 반환해 준다.
- 데코레이터를 사용할 때는 데코레이터에
()
(괄호)를 붙인 뒤 인수를 넣어주면 된다.
- 여기서는
is_multiple
에3
을 지정해서add
함수의 반환값이3
의 배수인지 확인했다. - 물론
is_multiple
에 다른 숫자를 넣으면 함수의 반환값이 해당 숫자의 배수인지 확인해 준다.
매개변수가 있는 데코레이터를 여러 개 지정하기
- 매개변수가 있는 데코레이터를 여러 개 지정할 때는 다음과 같이 인수를 넣은 데코레이터를 여러 줄로 지정해 준다.
@is_multiple(3)
@is_multiple(7)
def add(a, b):
return a + b
add(10, 20)
# add의 반환값은 7의 배수가 아닙니다.
# wrapper의 반환값은 3의 배수입니다.
@
을 사용하지 않았을 때는 다음 코드와 동작이 같다.
원래 함수 이름이 안나온다면?
- 데코레이터를 여러 개 사용하면 데코레이터에서 반환된
wrapper
함수가 다른 데코레이터로 들어간다. - 따라서 함수의
__name__
을 출력해 보면wrapper
가 나온다.
- 함수의 원래 이름을 출력하고 싶다면
functools
모듈의wraps
데코레이터를 사용해야 한다. - 다음과 같이
@functools.wraps
에func
를 넣은 뒤wrapper
함수 위에 지정해 준다. - 만약
from functools import wraps
로 데코레이터를 가져왔다면@wraps(func)
를 지정한다.
import functools
def is_multiple(x):
def real_decorator(func):
@functools.wraps(func)
def wrapper(a, b):
r = func(a, b)
if r % x == 0:
print(f"{func.__name__}의 반환값은 {x}의 배수입니다.")
else:
print(f"{func.__name__}의 반환값은 {x}의 배수가 아닙니다.")
return r
return wrapper
return real_decorator
@is_multiple(3)
@is_multiple(7)
def add(a, b):
return a + b
add(10, 20)
# add의 반환값은 7의 배수가 아닙니다.
# add의 반환값은 3의 배수입니다.
functools.wraps
는 원래 함수의 정보를 유지시켜준다.- 따라서 디버깅을 할 때 유용하므로 데코레이터를 만들 때는
@functools.wraps
를 사용하는 것이 좋다.
6. 클래스로 데코레이터 만들기
- 이번에는 클래스로 데코레이터를 만드는 방법을 알아보자.
- 특히 클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는
__call__
메서드를 구현해야 한다.
- 다음은 함수의 시작과 끝을 출력하는 데코레이터이다.
class Trace:
def __init__(self, func):
self.func = func
def __call__(self):
print(self.func.__name__, "함수 시작")
self.func()
print(self.func.__name__, "함수 끝")
@Trace
def hello():
print("hello")
hello()
# hello 함수 시작
# hello
# hello 함수 끝
- 클래스로 데코레이터를 만들 때는 먼저
__init__
메서드를 만들고 호출할 함수를 초깃값으로 받는다. - 그리고 매개변수로 받은 함수를 속성으로 저장한다.
- 이제 인스턴스를 호출할 수 있도록
__call__
메서드를 만든다. __call__
메서드에서는 함수의 시작을 알리는 문자열을 출력하고, 속성func
에 저장된 함수를 호출한다.- 그다음에 함수의 끝을 알리는 문자열을 출력한다.
def __call__(self):
print(self.func.__name__, "함수 시작")
self.func()
print(self.func.__name__, "함수 끝")
- 데코레이터를 사용하는 방법은 클로저 형태의 데코레이터와 같다.
- 호출할 함수 위에
@
을 붙이고 데코레이터를 지정하면 된다.
@
으로 데코레이터를 지정했으므로 함수는 그대로 호출해 준다.
- 참고로 클래스로 만든 데코레이터는
@
을 지정하지 않고, 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있다.
- 다음과 같이 데코레이터에 호출할 함수를 넣어서 인스턴스를 생성한 뒤 인스턴스를 호출해 주면 된다.
- 즉, 클래스에
__call__
메서드를 정의했으므로 함수처럼()
(괄호)를 붙여서 호출할 수 있다.
7. 클래스로 매개변수와 반환값을 처리하는 데코레이터 만들기
- 지금까지 클래스로 데코레이터를 만들어보았다.
- 클래스로 만든 데코레이터도 매개변수와 반환값을 처리할 수 있다.
- 다음은 함수의 매개변수를 출력하는 데코레이터이다.
class Trace:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
r = self.func(*args, **kwargs)
print(f"{self.func.__name__}(args={args}, kwargs={kwargs}) -> {r}")
return r
@Trace
def add(a, b):
return a + b
print(add(10, 20))
print(add(a=10, b=20))
# add(args=(10, 20), kwargs={}) -> 30
# 30
# add(args=(), kwargs={'a': 10, 'b': 20}) -> 30
# 30
- 클래스로 매개변수와 반환값을 처리하는 데코레이터를 만들 때는
__call__
메서드에 매개변수를 지정하고,self.func
에 매개변수를 넣어서 호출한 뒤에 반환값을 반환해 주면 된다. - 여기서는 매개변수를
*args
,**kwargs
로 지정했으므로self.func
에 넣을 때는 언패킹하여 넣어준다.
def __call__(self, *args, **kwargs):
r = self.func(*args, **kwargs)
print(f"{self.func.__name__}(args={args}, kwargs={kwargs}) -> {r}")
return r
- 물론 가변 인수를 사용하지 않고, 고정된 매개변수를 사용할 때는
def __call__(self, a, b):
처럼 만들어도 된다.
1) 클래스로 매개변수가 있는 데코레이터 만들기
- 마지막으로 매개변수가 있는 데코레이터를 만들어보자.
- 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터이다.
class IsMultiple:
def __init__(self, x):
self.x = x
def __call__(self, func):
def wrapper(a, b):
r = func(a, b)
if r % self.x == 0:
print(f"{func.__name__}의 반환값은 {self.x}의 배수입니다.")
else:
print(f"{func.__name__}의 반환값은 {self.x}의 배수가 아닙니다.")
return r
return wrapper
@IsMultiple(3)
def add(a, b):
return a + b
print(add(10, 20))
print(add(2, 5))
# add의 반환값은 3의 배수입니다.
# 30
# add의 반환값은 3의 배수가 아닙니다.
# 7
- 먼저
__init__
메서드에서 데코레이터가 사용할 매개변수를 초깃값으로 받는다. - 그리고 매개변수를
__call__
메서드에서 사용할 수 있도록 속성에 저장한다. - 지금까지
__init__
에서 호출할 함수를 매개변수로 받았는데, 여기서는 데코레이터가 사용할 매개변수를 받는다.
- 이제
__call__
메서드에서는 호출할 함수를 매개변수로 받는다. - 그리고
__call__
메서드 안에서wrapper
함수를 만들어준다. - 이때
wrapper
함수의 매개변수는 호출할 함수의 매개변수와 똑같이 지정해 준다.
wrapper
함수 안에서는func
의 반환값이 데코레이터 매개변수x
의 배수인지 확인한다.- 이때 데코레이터 매개변수
x
는 속성에 저장되어 있으므로self.x
와 같이 사용해야 한다. - 그리고 배수 확인이 끝났으면
func
의 반환값을 반환한다. - 마지막으로
wrapper
함수를 다 만들었으면return
으로wrapper
함수를 반환한다.
def __call__(self, func):
def wrapper(a, b):
r = func(a, b)
if r % self.x == 0:
print(f"{func.__name__}의 반환값은 {self.x}의 배수입니다.")
else:
print(f"{func.__name__}의 반환값은 {self.x}의 배수가 아닙니다.")
return r
return wrapper
- 데코레이터를 사용할 때는 데코레이터에
()
(괄호)를 붙인 뒤 인수를 넣어주면 된다.
- 보통 데코레이터는 프로그램의 버그를 찾는 디버깅, 함수의 성능 측정, 함수 실행 전에 데이터 확인 등에 활용한다.