본문 바로가기
Python

[Python] Decorator

by ds31x 2023. 8. 18.

Python이 제공하는 Decorator는 기존의 function을 수정하지 않으면서 특정 처리를 추가할 수 있게 해주는 도구라고 할 수 있다.

Decorate의 "꾸미다"라는 의미에 맞게 기존 function을 꾸며주는 기능을 제공한다.

Decorator를 사용하면 코드 중복을 효과적으로 줄여 보다 간결한 형태의 코딩이 가능함.

  • 특히 Python에서는 @를 통한 decorator 사용문법을 통해 매우 간결한 코딩이 가능함.
  • 특정 공통된 처리의 모듈화가 아주 효과적으로 가능하게 해줌.
  • 단점은 wrapper의 사용으로 debuging이 좀 까다로워짐(에러 위치를 찾는게 좀 더 까다로워짐.)
  • Closuer처럼 지나치게 많이 사용시 가독성이 떨어짐. 

참고로 이 문서에서는 function으로 decorator를 구현하는 방법을 설명한다.

class로 구현하는 경우는 다음 문서를 참고할 것.

2023.08.18 - [Python] - [Python] Class로 수행시간 측정 decorator 만들기

 

[Python] Class로 수행시간 측정 decorator 만들기

decorator가 유용하게 사용되는 경우 중 하나가 특정 function 등의 수행시간 측정이다. functoin으로 decorator를 만드는 경우에 대한 정리는 이전에 했기 때문에 여기선 class로 작성한다. 2023.08.18 - [Python]

ds31x.tistory.com


Applications

Class에서 method를 정의할 때, method head위에 놓여있던 @staticmethod, @classmethod, @abstractmethod 등이 바로 decorator 기능이며, 이들은 아래의 method에 특정 기능을 부가해준다.

class Test:
    @staticmethod
    def s_func():
        print('s_func is a static method!')
        return

이는 function에서도 사용가능하며 django의 built-in login_required decorator 등이 좋은 예이다.

from django.contrib.auth.decorators import login_required

@login_required
def post_detail(request: HttpRequest, pk: int) -> HttpResponse:
    # post = Post.objects.get(pk=pk)
    post = get_object_or_404(Post, pk=pk)  # 해당 object가 없으면 404 page로.
    return render(request, "blog/post_detail.html", {"post": post})

Decorator 구현하기 (전처리와 후처리 추가) : for Function

다음은 간단한 곱셈을 수행하는 function mul의 전 후에 특정 문자열이 출력되는 처리를 추가한 decorator의 예임.

  • 아래에 보이듯이 fist-class object로서의 function을 이용한 closure 로 구현이 되며 nested function인 warp_func를 통해 전처리후처리등이 추가될 수 있음.
  • nonlocal scope의 변수들을 통해 전역변수의 사용없이 특정 상태 등을 기억하게 할 수 있음.
def pre_post_deco(func):
  func_call_cnt = 0
  # nested function.
  def wrap_func(a,b): # 감싸주는 func 와 같은 parameters를 가짐.
    print('pre-processing')
    nonlocal func_call_cnt 
    func_call_cnt += 1
    print(f' - # of call :{func_call_cnt}') 

    ret_v = func(a,b)

    print('post-processing')
    print('===================')
    return ret_v
  return wrap_func

@pre_post_deco
def mul(a,b):
  return a*b

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

print(mul(5,2))
print(mul(5,3))
print(mul(5,4))
print(add(1,1))

# ===============================
# 다른 방식으로 decorator 활용하기.
def other_add(a,b):
  return a+b

wrapped_func = pre_post_deco(other_add)
print(wrapped_func(2,3))
  • muladd를 감싸고 있는 wrap_func function이 mul 과 같은 수의 parameters를 가지도록 한다.
  • func_call_cnt는 closure를 활용하여 @pre_post_deco로 감싸진 특정 function이 몇차례 호출되었는지를 기억함.
  • muladd는 각각 wrap_func로 감싸지며 이들은 고유의 func_call_cnt를 가지는 것을 주의할 것.

아래의 다른 방식으로 decorator 활용은 @pre_post_deco를 function head 위에 붙여놓을 경우 실제 이루어지는 처리를 코드로 구현한 것임. 감싸진 function을 반환받고 이를 호출하는 방식이 실제 decorator가 동작하는 방식임.

결과는 다음과 같음.

pre-processing
 - # of call :1
post-processing
===================
10
pre-processing
 - # of call :2
post-processing
===================
15
pre-processing
 - # of call :3
post-processing
===================
20
pre-processing
 - # of call :1
post-processing
===================
2
pre-processing
 - # of call :1
post-processing
===================
5

Parameter를 가지는 Decorator

parameter를 가지는 decorator의 경우, 해당 parameter에 따라 다른 동작을 수행할 수 있기 때문에 좀더 활용도가 높음.

위의 예에서 덧셈과 곱셈에 따라 다른 전처리를 해주기위해 pre_post_deco를 감싸는 pre_post_deoc_w_param을 통해,
parameter를 가지는 decorator를 구현한 예는 다음과 같음.

def pre_post_deco_w_param(target_func_str): # decorator에 필요한 parmaeters!
  # closure에서처럼, target_func_str은 nested functions에서 접근가능함.
  def pre_post_deco(func):
    func_call_cnt = 0
    # nested function.
    def wrap_func(a,b): # 감싸주는 func 와 같은 parameters를 가짐.
      print(f'{target_func_str}: pre-processing')
      nonlocal func_call_cnt 
      func_call_cnt += 1
      print(f' - # of call :{func_call_cnt}') 

      ret_v = func(a,b)

      print('post-processing')
      print('===================')
      return ret_v
    return wrap_func
  return pre_post_deco # 실제적인 deco 함수객체 반환.

@pre_post_deco_w_param('multiplication')
def mul(a,b):
  return a*b

@pre_post_deco_w_param('addition')
def add(a,b):
  return a+b

print(mul(5,2))
print(mul(5,3))
print(mul(5,4))
print(add(1,1))

decorator 중첩하기.

여러 decorator 또는 하나의 decorator를 여러차례 중첩하여 사용할 수 있음.

  • 결국 함수를 감싸주는 warpper함수를 다시 감싸주는 방식으로 동작함.

다음의 예를 보자.

@ pre_post_deco_w_param('test 01')
@ pre_post_deco_w_param('test 02')
def multiple_deco_func(a,b):
  return a-b

print(multiple_deco_func(10,9))
  • test 01을 argument로 가지는 경우가 test02를 argument로 가지는 경우를 감싸고 있음.
  • test 02를 argument로 가지는 경우는 실제 함수인 multiple_deco_func을 감싸게 됨.

위의 코드는 사실상 다음과 같음.

f = pre_post_deco_w_param('test 01') (pre_post_deco_w_param('test 02')(multiple_deco_func))
f(10,9)

결과는 다음과 같음.

test 01: pre-processing
 - # of call :1
test 02: pre-processing
 - # of call :1
post-processing
===================
post-processing
===================
1

Decorator 구현하기 (전처리와 후처리 추가) : for Method

method라고 큰 차이가 있진 않음.
단, 주의할 점 하나는 method의 첫번째 argument가 self라는 점을 반드시 기억해야 한다.
이는 method를 감싸주는 함수의 parameter에서 첫번째에 self 가 할당됨을 의미함.

 

다음 예로 확인하자.

def method_deco(method_obj):
  def wrap_method(self, a, b):
    print(f'pre-process : {method_obj.__name__}')
    r = method_obj(self, a, b)
    print(f'post-process : {method_obj.__name__}')
    print('===============================')
    return r

  return wrap_method

class Test:
  @method_deco
  def ds_pow(self, a, b):
    return a**b

o = Test()
print(o.ds_pow(4,5))

결과는 다음과 같음.

pre-process : ds_pow
post-process : ds_pow
===============================
1024

관련 GIST page

https://gist.github.com/dsaint31x/5acf386dbcba2450b388239d61ceedbd

 

py_decorator_func.ipynb

py_decorator_func.ipynb. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com


더 읽어보면 좋은 자료들.

2023.07.15 - [Python] - [Python] first-class object (일급객체)

 

[Python] first-class object (일급객체)

함수형 프로그래밍 언어에서 중요한 개념이며, JavaScript와 Python등에서 Closure와 같은 다양한 디자인패턴을 응용 및 구현하는데 중요한 역할을 한다. 함수형 언어 관련 참고 자료 : http://ds31x.blogspot

ds31x.tistory.com

2023.07.15 - [Python] - [Python] Closure

 

[Python] Closure

Closure의 정의는 다음과 같음. Nested function 으로, 자신의 enclosing scope (= Python에서 non-local scope)의 상태값(lexcical environment의 variable 값)을 기억하고 유지, 변경, 사용할 수 있는 경우를 가르킴. 2023.07.1

ds31x.tistory.com

2023.07.15 - [Python] - [Python] scope와 키워드 global, nonlocal

 

[Python] scope와 키워드 global, nonlocal

Python에서 scope는 namespace와 밀접하게 관련이 있는 개념이며, 이와 관련된 주요 키워드가 nonlocal과 global이 있음. https://dsaint31.tistory.com/entry/Basic-namespace-frame-and-context [Basic] namespace, frame, and context Nam

ds31x.tistory.com

2023.07.15 - [Python] - [Python] Nested Function

 

[Python] Nested Function

nested는 중첩이라고 불리며 일반적으로 2중 loop등을 지칭할 때 사용되는 용어임. nested function이란, `for`문 안에 `for`문이 중첩되어 2중 loop를 구성하는 것처럼, function 내부에서 function을 정의(선언)

ds31x.tistory.com

https://cjh5414.github.io/wraps-with-decorator/

 

Python decorator에 @wraps를 사용해야 하는 이유

Jihun's Development Blog

cjh5414.github.io