Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Programming/Python] 데코레이터 (decorator) 본문

Programming/Python

[Programming/Python] 데코레이터 (decorator)

scii 2023. 1. 31. 01:56

파이썬은 데코레이터라는 기능을 제공한다. 데코레이터는 장식하다, 꾸미다라는 뜻의 decorate에 er(or)을 붙인  말인데 장식하는 도구 정도로 설명할 수 있다.

클래스에서 메서드를 만들 때 @staticmethod, @classmethod, @abstractmethod 등을 붙였는데, 이렇게 @로 시작하는 것들이 데코레이터이다. 즉, 함수(메서드)를 장식한다고 해서 이런 이름이 붙었다.

class Calc:
    @staticmethod    # 데코레이터
    def add(a, b):
        print(a + b)

데코레이터를 만드는 방법

데코레이터는 함수를 장식한다고 했는데 도대체 어디에 사용하는 것일까? 데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용한다. 예를 들어 함수의 시작과 끝을 출력하고 싶다면 다음과 같이 함수 시작, 끝부분에 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 함수 종료

먼저 데코레이터 trace는 호출할 함수를 매개변수로 받는다. trace 함수 안에서는 호출할 함수를 감싸는 함수 wrapper를 만든다. 
wrapper 함수에서는 함수의 시작을 알리는 문자열을 출력하고, trace에서 매개변수로 받는 func를 호출한다. 그 다음 함수의 종료를 알리는 문자열을 출력한다. 마지막으로 wrapper 함수를 return하여 wrapper 함수 자체를 반환한다. 즉, 함수 안에서 함수를 만들고 반환하는 클로저이다.


@으로 데코레이터 사용하는 방법

이제 @을 사용하여 좀 더 간편하게 데코레이터를 사용해보자. 다음과 같이 호출할 함수 위에 @데코레이터 형식으로 지정한다.

@데코레이터
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 함수 종료

hello와 world 함수 위에 @trace를 붙인 뒤에 hello와 world 함수를 호출하면 끝이다. 이렇게 데코레이터는 함수를 감싸는 형태로 구성되어 있다. 따라서 데코레이터는 기존 함수를 수정하지 않으면서 추가 기능을 구현할 때 사용한다.


데코레이터 여러 개 지정

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

@데코레이터
@데코레이터
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

매개변수와 반환값을 처리하는 데코레이터 만들기

매개변수와 반환값을 처리하는 데코레이터는 어떻게 만드는지 알아보자. 다음은 함수의 매개변수와 반환값을 출력하는 데코레이터다.

def trace(func):
    def wrapper(a, b):
        r = func(a, b)
        print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))
        return r
    return wrapper

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


# 결과
add(a=2, b=5) -> 7
7

add 함수를 호출했을 때 데코레이터를 통해 매개변수와 반환값이 출력되었다. 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 안쪽 wrapper 함수의 매개변수를 호출할 함수 add(a, b)의 매개변수와 똑같이 만들어준다.

wrapper 함수 안에서는 func를 호출하고 반환값을 변수에 저장한다. 그 다음 print로 매개변수와 반환값을 출력한다. 이때 func에는 매개변수 a, b를 그대로 넣어준다. 또한 add 함수는 두 수를 더해 반환해야 하므로 func의 반환값을 return으로 반환해준다.

만약 wrapper함수에서 func의 반환값을 반환하지 않으면 add 함수를 호출해도 반환값이 나오지 않으므로 주의해야 한다.


가변 인수 함수 데코레이터

def add(a, b)는 매개변수의 개수가 고정된 함수이다. 매개변수가 고정되지 않은 함수는 어떻게 처리할까? 이때는 wrapper 함수를 가변 인수 함수로 만들면 된다.

def trace(func):
    def wrapper(*args, **kwargs):
        r = func(*args, **kwargs)
        print('{0}(args={1}, kwargs={2}) -> {3}'.format(func.__name__, args, 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(5, 7))
print(get_min(x=10, y=20, z=30))


# 결과
get_max(args=(5, 7), kwargs={}) -> 7
7
get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
10

매개변수가 있는 데코레이터 만들기

데코레이터는 값을 지정해서 동작을 바꿀 수 있다. 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터이다.

def is_multiple(x):
    def real_decorator(func):
        def wrapper(a, b):
            r = func(a, b)
            if r % x == 0:
                print('{0}의 반환값은 {1}의 배수이다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아니다.'.format(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):     # 호출할 함수의 매개변수와 똑같이 지정

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

@데코레이터(인수)
def 함수이름():
    코드

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

@데코레이터1(인수)
@데코레이터2(인수)
def 함수이름():
    코드

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

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

함수의 원래 이름을 출력하고 싶다면 functools 모듈의 wraps 데코레이터를 사용해야 한다.

import functools

def is_multiple(x):
    def real_decorator(func):
        @functools.wraps(func)      # @functools.wraps에 func를 넣은 뒤 wrapper 함수 위에 지정
        def wrapper(a, b):
            r = func(a, b)
            if r % x == 0:
                print('{0}의 반환값은 {1}의 배수이다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아니다.'.format(func.__name__, x))
            return r
        return wrapper
    return real_decorator

@functools.wraps는 원래 함수의 정보를 유지시켜준다. 따라서 디버깅할 때 유용하므로 데코레이터를 만들 때는 @functools.wraps를 사용하는 것이 좋다.


클래스로 데코레이터 만들기

클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는 __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에 저장된 함수를 호출한다. 그 다음 함수의 종료를 알리는 문자열을 출력한다.

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

참고로 클래스로 만든 데코레이터는 @을 지정하지 않고, 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있다. 다음과 같이 데코레이터에 호출할 함수를 넣어서 인스턴스를 생성한 뒤 인스턴스를 호출해주면 된다. 즉, 클래스에 __call__ 메서드를 정의했으므로 함수처럼 ()를 붙여서 호출할 수 있다.

def hello():
    print('hello')
    
trace_hello = Trace(hello)
trace_hello()    # 인스턴스를 호출. __call__ 메서드가 호출됨.

클래스로 매개변수와 반환값을 처리하는 데코레이터

클래스로 만든 데코레이터도 매개변수와 반환값을 처리할 수 있다. 다음은 함수의 매개변수를 출력하는 데코레이터이다.

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

    def __call__(self, *args, **kwargs):
        r = self.func(*args, **kwargs)
        print('{0}(args={1}, kwargs={2}) -> {3}'.format(self.func.__name__, args, 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에 매개변수를 넣어서 호출한 뒤에 반환값을 반환해주면 된다. 물론 가변 인수를 사용하지 않고, 고정된 매개변수를 사용할 때는 def __call__(self, a, b) 처럼 만들어도 된다.


클래스로 매개변수가 있는 데코레이터 만드는 방법

매개변수가 있는 데코레이터를 만들어보자. 다음은 함수의 반환값이 특정 수의 배수인지 확인하는 데코레이터이다.

import functools

class IsMultiple:
    def __init__(self, x):
        # 데코레이터가 사용할 매개변수를 초깃값으로 받음
        self.__x = x

    # 호출할 함수를 매개변수로 받음
    def __call__(self, func):
        # 호출할 함수의 매개변수와 똑같이 지정
        functools.wraps(func)
        def wrapper(a, b):
            r = func(a, b)
            if r % self.__x == 0:
                print('{0}의 반환값은 {1}의 배수이다.'.format(func.__name__, self.__x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아니다.'.format(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 type_check(type_a, type_b):
    def real_decorator(func):
        def wrapper(a, b):
            if isinstance(a, type_a) and isinstance(b, type_b):
                return func(a, b)
            else:
                raise RuntimeError('자료형이 올바르지 않습니다.')
        return wrapper
    return real_decorator
Comments