Python

데코레이터(장식자)

haventmetyou 2023. 10. 25. 13:50

장식하는 도구

클래스에서 메서드를 만들 때 @staticmethod, @classmethod, @abstractmethod 등을 붙였는데 @로 시작하는 것들이 데코레이터

> 함수(메서드)를 장식

 

데코레이터 만들기

데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용

예를 들어 지금까지는 시작과 끝을 출력하고 싶다면 함수 시작, 끝 부분에 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 함수 끝

이런 경우 데코레이터 사용

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                           # 호출할 함수를 감싸는 함수
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # 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는 추적하다란 뜻, 프로그래밍에서 함수의 실행 상황을 추적할 때 trace 사용)

wrapper는 함수를 감싼다는 뜻에서 사용했는데, 다른 이름 사용해도 상관없음

매개변수로 받은 함수의 원래 이름 출력할 때 __name__ 속성 활용

> 함수 안에서 함수를 만들고 반환하는 클로저

 

@으로 데코레이터 사용

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

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():                          
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환

@trace    # @데코레이터
def hello():
    print('hello')

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

hello()
world()

# 실행 결과

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

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

 

데코레이터 여러 개 지정

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

 

매개변수와 반환값을 처리하는 데코레이터 만들기
def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
        r = func(a, b)    # func에 매개변수 a, b 넣어 호출하고 반환값 변수에 저장
        print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))
                          # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # wrapper 함수 반환

@trace            # @데코레이터
def add(a, b):    # 매개변수 두 개
    return a + b

print(add(10, 20))

# 실행 결과

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

wrapper 함수에서 func의 반환값을 반환하지 않으면 add 함수를 호출해도 반환값이 나오지 않으므로 주의할 것

wrapper 함수에서 func의 반환값을 출력할 필요 없다면 return func(a, b)처럼 func를 호출해 바로 반환해도 됨

 

가변 인수 함수 데코레이터

def trace(func):
    def wrapper(*args, **kwargs):    # 가변 인수 함수로 만듦
        r = func(*args, **kwargs)    # func에 args, kwargs를 언패킹해 넣어 줌
        print('{0}(args={1}, kwargs={2}) -> {3}'.format(func.__name__, args, kwargs, r))
                          # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # 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

 

매개변수가 있는 데코레이터
def is_multiple(x):              # 데코레이터가 사용할 매개변수 지정
    def real_decorator(func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정
            r = func(a, b)       # func 호출, 반환값 변수에 저장
            if r % x == 0:
                print('{0}의 반환값은 {1}의 배수'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아님'.format(func.__name__, x))
            return r             # func의 반환값 반환
        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

앞에서는 데코레이터 만들 때 함수 안에 함수를 하나만 만들었지만, 매개변수 있는 데코레이터를 만들 때는 함수를 하나 더 만들어야 함

 

1. is_multiple 함수를 만들고 데코레이터가 사용할 매개변수 x 지정

2. is_multiple 함수 안에서 실제 데코레이터 역할을 하는 real_decorator 만듦

> 이 함수에서 호출할 함수를 매개변수로 받음

3. real_decorator 함수 안에서 wrapper 함수 만듦

 

데코레이터 사용할 때는

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

 

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

클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해 주는 __call__ 메서드 구현

class Trace:
    def __init__(self, func):    # 호출할 함수를 인스턴스의 초깃값으로 받음
        self.func = func         # 호출할 함수를 속성 func에 저장
    
    def __call__(self):
        print(self.func.__name__, '함수 시작')
        self.func()
        print(self.func.__name__, '함수 끝')

@Trace
def hello():
    print('hello')

hello()

# 실행 결과

hello 함수 시작
hello
hello 함수 끝

↑ 함수의 시작과 끝을 출력하는 데코레이터

클로저 형태의 데코레이터와 사용하는 방법이 같음

 

@을 지정하지 않고 데코레이터의 반환값을 호출하는 방식으로도 사용할 수 있음

def hello():    # @데코레이터를 지정하지 않음
    print('hello')

trace_hello = Trace(hello)    # 데코레이터에 호출할 함수를 넣어 인스턴스 생성
trace_hello()                 # 인스턴스를 호출 .__call__ 메서드가 호출됨

 

클래스로 매개변수와 반환값을 처리하는 데코레이터 만들기
class Trace:
    def __init__(self, func):    # 호출할 함수를 인스턴스의 초깃값으로 받음
        self.func = func         # 호출할 함수를 속성 func에 저장
    
    def __call__(self, *args, **kwargs):    # 호출할 함수의 매개변수를 처리
        r = self.func(*args, **kwargs)      # self.func에 매개변수를 넣어서 호출
                                            # 반환값 변수에 저장
        print('{0}(args={1}, kwargs={2}) -> {3}'.format(self.func.__name__, args, kwargs, r))
                                            # 매개변수와 반환값 출력
        return r                            # self.func의 반환값 반환
        
@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

 

클래스로 매개변수가 있는 데코레이터 만들기

class IsMultiple:
    def __init__(self, x):    # 데코레이터가 사용할 매개변수를 초깃값으로 받음
        self.x = x            # 매개변수를 속성 x에 저장(__call__ 메서드에서 사용할 수 있도록)
    
    def __call__(self, func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정

            r = func(a, b)
            if r % self.x == 0:
                print('{}의 반환값은 {}의 배수'.format(func.__name__, self.x))
            else:
                print('{}의 반환값은 {}의 배수가 아님'.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