파이썬을 배워보자 16일차 - 함수 Decorator

0

Python

목록 보기
16/18

점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378
GeeksforGeeks : https://www.geeksforgeeks.org/decorators-in-python/

함수 데코레이터(Decorator)

java로 개발을 하다보면 decorator 문법을 많이 사용하게 되는데, python에서도 decorator문법을 지원한다.

decorator문법은 특별한 것은 아니고 이전에 사용했던 @staticmethod, @classmethod, @absctractmethod 등과 같이, 함수 위에 얹혀서 사용되는 것을 말한다.

decorator문법을 사용하는 가장 큰 이유는 기존에 정의된 함수의 내용을 수정하지 않고 추가적인 내용, 기능을 덧붙일 수 있기 때문이다.

def process(a, b):
    ret = a + b
    print("ret: " + str(ret))

가령, process함수의 시작, 을 기록하는 로깅을 하고 싶다고 하자. 그렇다면 다음과 같이 써줄 수 있다.

  • eaxmple.py
def start():
    print("func started!")

def end():
    print("func completed!")

def process(a, b):
    start()
    ret = a + b
    print("ret: " + str(ret))
    end()

process(1,2)
  • 결과
func started!
ret: 3
func completed!

다음과 같이 process함수의 앞 뒤 부분에 start, end 함수를 넣어주는 수 밖에 없다. 그러나, process와 같은 함수들이 많아지면 많아질수록 이러한 방식은 매우 손이 많이 가는 일이며, 또한 process라는 함수가 가진 concern이 훼손될 수 있다. concern은 함수가 관장하고 있는 관심사로 process는 계산이라는 concern에만 집중하면 되는데 여기서는 logging이라는 로직도 들어가게되어 code를 읽는데 있어 가독성이 떨어질 수 있다.

python의 함수에는 다음과 같은 특성이 있다.

  1. 함수는 first-class objects(string, int, float, list and so on)이다. 즉, 다른 object들과 같이 arguments로 함수가 사용될 수 있고, 반대로 함수를 반환할 수도 있다.
  2. 함수 안에 함수를 선언할 수 있다.

이 두가지 특성을 이용하여 위 문제를 해결해보자.

  • example.py
def logging(func, a, b):
    def start():
        print("func started!")

    def end():
        print("func completed!")
    
    def wrapper():
        start()
        func(a, b)
        end()
    return wrapper 

def process(a, b):
    ret = a + b
    print("ret: " + str(ret))

log_process = logging(process, 1 ,2)
log_process()
  • 결과
func started!
ret: 3
func completed!

기존에 만들었던 process는 아무런 변경이 없고, 이 함수를 wrapping하도록 하여 log 기능을 추가하였다. 이것이 가능한 이유는 python은 함수를 매개변수로 받을 수 있고, 또는 반환이 가능하다는 것과 함수 안에 inner 함수를 선언할 수 있다는 것이다.

logging함수는 매개변수로 func을 받는다. 이 func을 실제로 wrapping하는 함수는 wrapper이다. wrapperlogging함수의 inner 함수이고, 반환값이다. 입력으로 받은 func 앞 뒤에 start, end를 넣은 wrapper 함수를 반환하고 이 함수를 log_process로 받아 실행하면 원하는 결과가 나오게 되는 것이다.

1. Decorator 사용법

위의 패턴을 간단하게 만들어주는 것이 바로 Decorator이다. decorator는 decorator함수와 이를 호출하는 @함수가 있다. wrapper를 반환하는 logging함수가 바로 decorator함수가 되고, @함수process위에 적어주기만 하면 된다.

processa+b로직이 아니라, 단순 print만 하도록 로직을 변경하고, decorator를 적용한 코드를 만들어보자.

  • example.py
def logging(func):
    def start():
        print("func started!")

    def end():
        print("func completed!")
    
    def wrapper():
        start()
        func()
        end()
    return wrapper 

@logging
def process():
    print("start!")

process()
  • 결과
func started!
start!
func completed!

굉장히 간단하다. loggingdecorator 함수이고, @logging이 바로 @함수process 함수 위에 적어주면 process 함수를 입력으로 받아 wrapper가 반환되어 실행된다.

재밌는 것은 decorator를 여러개 지정할 수 있다는 것이다. 가령, 이번에는 시간을 관리하는 코드를 넣겠다고 한다면 다음과 같이 할 수 있다.

  • example.py
from datetime import datetime
def logging(func):    
    def wrapper():
        print("func started!")
        func()
        print("func completed!")
    return wrapper 

def time_logging(func):
    def wrapper():
        now = datetime.now()
        print("start Time =" ,now.strftime("%H:%M:%S"))
        func()
        now = datetime.now()
        print("end Time =" ,now.strftime("%H:%M:%S"))
    return wrapper

@time_logging
@logging
def process():
    print("start!")

process()
  • result
start Time = 16:17:03
func started!
start!
func completed!
end Time = 16:17:03

이렇게 여러개의 decorator가 하나의 함수에 얹혀서 사용될 수 있는 이유는 함수를 일급 함수, 일급 객체로 취급하여 매개변수로 받고, 반환이 가능하기 때문이다. 사실 위의 process() 코드는
wrapper(wrapper(process)) 와 같다.

2. Decorator에 매개변수, 반환값 처리하기

매개변수를 decorator 함수에 넣어주는 방법은 decorator 함수의 반환 함수인 wrapper 함수에 똑같이 매개변수를 추가해주면 된다. 반환값 역시도 마찬가지이다. wrapper 에서 return해줄 값을 써주면 된다.

  • example.py
from datetime import datetime
def logging(func):    
    def wrapper(a,b): # 매개변수 a, b 가 추가됨
        print("func started!")
        ret = func(a,b)
        print("func completed!")
        return ret*2 # 반환값으로 process에 2를 곱한 값을 반환한다.
    return wrapper 

@logging
def process(a, b):
    ret = a + b
    print("start! ", ret)
    return ret

double_value = process(10,2)
print(double_value)
  • 결과
func started!
start!  12
func completed!
24

위와 같이 wrapper로 반환되는 함수에 매개변수만 추가하면 process에 들어간 매개변수 a, b를 사용할 수 있다. 또한, process의 반환값에 두 배를 곱하여 반환하고 싶다면 wrapper의 반환값으로 process의 반환값에 2를 곱해주면 된다.

그런데 만약, 가변 인수인 경우는 어떻게 해야할까?? 이는 wrapper 함수 역시도 가변 인수 함수로 만들어주면 된다. 참고로 가변 인수와 가변 키워드 인수를 소개하면 다음과 같다.

  • 가변 인수
  1. 함수 선언 시 매개변수 앞에 *을 붙이며 튜플 형식으로 값이 들어간다. 이를 packing이라고 한다.
  2. 함수 호출 시 매개변수 앞에 *을 붙이면 튜플 형식의 값이 풀어져서 함수의 매개변수로 들어간다. 이를 unpacking이라고 한다.
  • 가변 키워드 매개변수
  1. 함수 선언 시 매개변수 앞에 **을 붙이며 dictionary 형식으로 값이 들어간다. 이를 packing이라고 한다.
  2. 함수 호출 시 매개변수 앞에 **을 붙이면 dictionary 형식의 값을 unpacking 하여 넣는다.
from datetime import datetime
def logging(func):    
    def wrapper(*args, **kwargs): # 가변 인수 매개변수 args와 가변 키워드 매개변수 kwargs를 추가한다.
        print("func started!")
        ret = func(*args, **kwargs)
        print("func completed!")
        return ret
    return wrapper 

@logging
def get_min(*args):
    return min(args)

@logging
def get_max(**kwargs):
    return max(kwargs.values())

print(get_min(10,20,30))
print(get_max(x=2,y=20,z=24))
  • 결과
func started!
10
func started!
func completed!
24

다음과 같이 문제 없이 구동되는 것을 확인할 수 있다.

메서드에 decorator을 사용할 때는 주의해야한다. 클래스를 만들면서 메서드에 decorator를 사용할 때는 self에 주의해야 한다. 클래스의 인스턴스 메서드는 항상 self를 받으므로 decorator를 만들 때에도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 한다. 또한, 클래스 메서드도 마찬가지로 cls를 사용하므로 이를 decorator에 넣어야 한다.

  • example.py
def logging(func):
    def wrapper(self, a, b):
        print("start!")
        r = func(self, a, b)
        print("end!")
        return r
    return wrapper

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

c = Calc()
print(c.add(10,20))
  • 결과
start!
end!
30

3. 매개변수가 있는 decorator

매개변수가 있는 데코레이터를 만드려면, wrapper함수 위를 덮고 있던 decorator 함수 위에 또 하나의 함수를 wrapping해야주야 한다. 즉, 다음과 같은 구조가 된다.

def para_decorator(x): # decorator의 파라미터를 받는 wrapper
    def decorator(func): # decorator
        def wrapper(a,b): # wrapper
            #...
            r = func(a,b)
            #...
            return r
        return wrapper
    return decorator # decoraotr를 반환한다.

@para_decorator(10) # decorator에 바로 매개변수를 써주면 된다.
def process(a,b):
    #...

decorator에 파라미터가 존재하게되면 이제 decorator는 3단계로 이루어지게 된다. 첫번재 단계는 매개변수를 받는 wrapper, 두 번째 단계는 wrapping할 함수를 받는 wrapper, 세 번째 단계는 wrapping할 함수를 wrap하는 wrapper이다. 조금은 복잡해보여도 규칙은 간단하다.

이제 매개변수가 있는 decorator에 매개변수를 넣어보자, @decorator(value)이런식으로 넣어주면 된다. 만약 keyword라면 keyword를 넣어주면 된다.

이를 활용하여 decorator로 넣은 매개변수로 결과값을 곱해주는 로직을 만들어보자.

  • example.py
def multiple_by(x):
    def decorator(func):
        def wrapper(a, b):
            print("start!")
            r = func(a, b)
            print("end!")
            return r * x
        return wrapper
    return decorator

@multiple_by(10)
def process(a, b):
    return a + b

print(process(10,2))
  • 결과
start!
end!
120

다음과 같이 활용할 수 있다.

0개의 댓글