Skip to content

30. 데코레이터(Decorator)


1. 키워드

  • 데코레이터(Decorator)
  • 클로저(Closure)


2. 데코레이터 사용하기

  • 지금까지 클래스에서 메서드를 만들 때 @staticmethod, @classmethod, @abstractmethod 등을 붙였는데, 이렇게 @로 시작하는 것들이 데코레이터이다.


  • 즉, 다음과 같이 함수(메서드)를 장식한다고 해서 이런 이름이 붙었다.


class Calc:
    @staticmethod
    def add(a, b):
        print(a + b)


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 함수 끝


  • helloworld 함수의 시작과 끝이 출력되었다.


  • 먼저 데코레이터 trace는 다음과 같이 호출할 함수를 매개변수로 받는다.
  • trace는 추적하다라는 뜻인데, 프로그래밍에서 함수의 실행 상황을 추적할 때 trace라는 말을 사용한다.


def trace(func):


  • trace 함수 안에서는 호출할 함수를 감싸는 함수 wrapper를 만든다.


    def 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_hello = trace(hello)
trace_hello()
trace_world = trace(world)
trace_world()


  • 물론 trace에 다른 함수를 넣은 뒤 반환된 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있다.


1) @로 데코레이터 사용하기

  • 이제 @을 사용하여 좀 더 간편하게 데코레이터를 사용해 보자.


  • 다음과 같이 호출할 함수 위에 @데코레이터 형식으로 지정한다.


@데코레이터
def 함수이름():
    코드
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 함수 끝


  • helloworld 함수 위에 @trace를 붙인 뒤에 helloworld 함수를 그대로 호출하면 끝이다.


@trace
def hello():
    print("hello")

@trace
def world():
    print("world")

hello()
world()


  • 물론 다른 함수 위에 @trace를 붙인 뒤 함수를 호출하면 해당 함수의 시작과 끝을 출력할 수 있다.


  • 이 데코레이터를 그림으로 표현하면 다음과 같은 모양이 된다.


001


  • 이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있다.
  • 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용한다.


데코레이터를 여러 개 지정하기

  • 함수에는 데코레이터를 여러 개 지정할 수 있다.


  • 다음과 같이 함수 위에 데이터를 여러 줄로 지정해 주는데, 이때 데코레이터가 실행되는 순서는 위에서 아래 순이다.


@데코레이터1
@데코레이터2
def 함수이름():
    코드
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


  • @을 사용하지 않았을 때는 다음 코드와 동작이 같다.


decorated_hello = decorator1(decorator2(hello))
decorated_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)의 매개변수와 똑같이 만들어준다.


def trace(func):
    def wrapper(a, b):


  • wrapper 함수 안에서는 func를 호출하고 반환값을 변수에 저장한다.
  • 그다음에 print로 매개변수와 반환값을 출력한다.
  • 이때 func에는 매개변수 ab를 그대로 넣어준다.
  • 또한, 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 함수는 그대로 호출해 준다.


@trace
def add(a, b):
    return a + b


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를 지정해 준다.


def trace(func):
    def wrapper(*args, **kwargs):


  • 이렇게 만든 데코레이터 trace는 위치 인수와 키워드 인수를 모두 처리할 수 있다.
  • 따라서 가변 인수 함수뿐만 아니라 일반적인 함수에도 사용할 수 있다.


@trace
def add(a, b):
    return a + b

add(10, 20)

# add(args=(10, 20), kwargs={}) -> 30
# 30


메서드에 데코레이터 사용하기

  • 클래스를 만들면서 메서드에 데코레이터를 사용할 때는 self를 주의해야 한다.
  • 인스턴스 메서드는 항상 self를 받으므로 데코레이터를 만들 때도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 하고, 반대로 클래스 메서드라면 cls로 지정해야 한다.
  • 마찬가지로 func를 호출할 때도 self와 매개변수를 그대로 넣어야 한다.


def trace(func):
    def wrapper(self, a, b):
        r = func(self, a, b)
        print(f"{func.__name__}(a={a}, b={b}) -> {r}")
        return r
    return wrapper

class Calc:
    @trace
    def add(self, a, b):
        return a + b

c = Calc()
print(c.add(10, 20))

# add(a=10, b=20) -> 30
# 30


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 함수를 만들어주면 된다.


def is_multiple(x):
    def real_decorator(func):
        def wrapper(a, b):


  • 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으로 두 함수를 반환해 준다.


        return wrapper
    return real_decorator


  • 데코레이터를 사용할 때는 데코레이터에 ()(괄호)를 붙인 뒤 인수를 넣어주면 된다.


@데코레이터(인수)
def 함수이름():
    코드
@is_multiple(3)
def add(a, b):
    return a + b


  • 여기서는 is_multiple3을 지정해서 add 함수의 반환값이 3의 배수인지 확인했다.
  • 물론 is_multiple에 다른 숫자를 넣으면 함수의 반환값이 해당 숫자의 배수인지 확인해 준다.


매개변수가 있는 데코레이터를 여러 개 지정하기

  • 매개변수가 있는 데코레이터를 여러 개 지정할 때는 다음과 같이 인수를 넣은 데코레이터를 여러 줄로 지정해 준다.


@데코레이터1(인수)
@데코레이터2(인수)
def 함수이름():
    코드
@is_multiple(3)
@is_multiple(7)
def add(a, b):
    return a + b

add(10, 20)

# add의 반환값은 7의 배수가 아닙니다.
# wrapper의 반환값은 3의 배수입니다.


  • @을 사용하지 않았을 때는 다음 코드와 동작이 같다.


decorated_add = is_multiple(3)(is_multiple(7)(add))
decorated_add(10, 20)


원래 함수 이름이 안나온다면?

  • 데코레이터를 여러 개 사용하면 데코레이터에서 반환된 wrapper 함수가 다른 데코레이터로 들어간다.
  • 따라서 함수의 __name__을 출력해 보면 wrapper가 나온다.


# add의 반환값은 7의 배수가 아닙니다.
# wrapper의 반환값은 3의 배수입니다.


  • 함수의 원래 이름을 출력하고 싶다면 functools 모듈의 wraps 데코레이터를 사용해야 한다.
  • 다음과 같이 @functools.wrapsfunc를 넣은 뒤 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__ 메서드를 만들고 호출할 함수를 초깃값으로 받는다.
  • 그리고 매개변수로 받은 함수를 속성으로 저장한다.


class Trace:
    def __init__(self, func):
        self.func = func


  • 이제 인스턴스를 호출할 수 있도록 __call__ 메서드를 만든다.
  • __call__ 메서드에서는 함수의 시작을 알리는 문자열을 출력하고, 속성 func에 저장된 함수를 호출한다.
  • 그다음에 함수의 끝을 알리는 문자열을 출력한다.


    def __call__(self):
        print(self.func.__name__, "함수 시작")
        self.func()
        print(self.func.__name__, "함수 끝")


  • 데코레이터를 사용하는 방법은 클로저 형태의 데코레이터와 같다.
  • 호출할 함수 위에 @을 붙이고 데코레이터를 지정하면 된다.


@데코레이터
def 함수이름():
    코드
@Trace
def hello():
    print("hello")


  • @으로 데코레이터를 지정했으므로 함수는 그대로 호출해 준다.


hello()


  • 참고로 클래스로 만든 데코레이터는 @을 지정하지 않고, 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있다.


  • 다음과 같이 데코레이터에 호출할 함수를 넣어서 인스턴스를 생성한 뒤 인스턴스를 호출해 주면 된다.
  • 즉, 클래스에 __call__ 메서드를 정의했으므로 함수처럼 ()(괄호)를 붙여서 호출할 수 있다.


def hello():
    print("hello")

trace_hello = Trace(hello)
trace_hello()


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__에서 호출할 함수를 매개변수로 받았는데, 여기서는 데코레이터가 사용할 매개변수를 받는다.


    def __init__(self, x):
        self.x = x


  • 이제 __call__ 메서드에서는 호출할 함수를 매개변수로 받는다.
  • 그리고 __call__ 메서드 안에서 wrapper 함수를 만들어준다.
  • 이때 wrapper 함수의 매개변수는 호출할 함수의 매개변수와 똑같이 지정해 준다.


    def __call__(self, func):
        def wrapper(a, b):


  • 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


  • 데코레이터를 사용할 때는 데코레이터에 ()(괄호)를 붙인 뒤 인수를 넣어주면 된다.


@데코레이터(인수)
def 함수이름():
    코드
@IsMultiple(3)
def add(a, b):
    return a + b


  • 보통 데코레이터는 프로그램의 버그를 찾는 디버깅, 함수의 성능 측정, 함수 실행 전에 데이터 확인 등에 활용한다.

References