본문 바로가기
Python/Python 개념

파이썬 - 데코레이터

by hyun-am 2020. 9. 14.

 

데코레이터(Decorator)란?

 

파이썬 데코레이터란 호출 가능 객체(함수, 메서드, 클래스)를 영구적으로 수정하지 않고도 그 동작을 확장, 수정할 수 있게 합니다.

데코레이터를 사용하는 예시는 아래와 같스빈다.

  • 로그 남기기
  • 접근 제어와 인증 시행
  • 계측 및 시간 측정
  • 비율 제한
  • 캐싱 및 기타

 

데코레이터를 사용하는 이유

 

파이썬 데코레이터를 사용하는 이유는 예를 들면 비즈니스 로직이나 인증이 담긴 함수가 있는데 이제 50개 정도 함수에 이것 로직을 넣으려고 하면 굳이 일일이 넣을 필요 없이 데코레이터를 이용하면 깔끔하고 간단하게 처리할 수 있습니다.

 

데코레이터 기초 실습

 

먼저 기본적인 데코레이터는 호출 가능 객체를 입력받아 다른 호출 가능 객체를 반환하는 호출 가능한 객체입니다. 먼저 아래와 같은 코드는 가장 간단한 데코레이터라고 할 수 있습니다.

def null_decorator(func):
    return func

보이는 대로 null_decorator는 호출 가능하며(함수) 다른 호출 가능 객체를 입력받아 그 호출 가능 객체를 수정하지 않고 반환합니다.

이제 이것을 이용해 다른 함수를 꾸며 보겠습니다.

 

@ 구문 없이 꾸미기

def greet():
    return "Hello!"

greet = null_decorator(greet)
print(greet())
# 출력 값
# Hello!

 

@ 구문 있이 꾸미기

@null_decorator
def greet():
    return "Hello!"


print(greet())
# 출력 값
# Hello!

@ 구문을 사용하면 정의 시간에 즉시 함수가 장식됩니다. 이로 인해 까다로운 해킹만 하지 않으면 장식되지 않은 원본에 접근하기가 어려워 집니다. 만약 원본을 사용하고 싶으면 @ 구문을 사용하지 않고 직접 장식하면 됩니다.

 

데코레이터를 통한 동작 수정하기

 

이제 기초적인거 말고 뭔가 함수 동작을 수정하는 데코레이터를 작성하겠습니다. 장식된 함수의 결과를 모두 소문자로 바꾸는 데코레이터를 만들겠습니다.

def lowercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.lower()
        return modified_result
    return wrapper

여기서 봐야할 점은 새로운 함수(클로저)를 즉석에서 정의해 입력 함수를 감쌈으로써, 나중에 호출될 때 함수의 동작이 달라지게 합니다.

wrapper 클로저는 장식되지 않은 입력 함수에 접근할 수 있으며 입력함수를 호출하기 전에 자유롭게 실행할 수 있습니다.

이제 lowercase 데코레이터가 잘 작동하는지 확인하겠습니다.

@lowercase
def greet():
    return "HELLO!!"

print(greet())

# 출력 값
# hello!!

출력 값이 소문자로 잘 변경된 것을 확인할 수 있습니다.

 

다중 데코레이터를 함수에 적용하기

 

이제 두개의 데코레이터를 만들어서 데코레이터가 중복으로 함수에 적용되는지 확인하겠습니다.

def strong(func):
    def wrapper():
        return '<strong>' + func() + '</strong>'
    return wrapper

def emphasize(func):
    def wrapper():
        return '<em>' + func() + '</em>'
    return wrapper

먼저 이런식으로 데코레이터를 선언한 후 아래 함수에다가 꾸며주겠습니다.

@strong
@emphasize
def greet():
    return "Hello!"

print(greet())
# 출력 값
# <strong><em>Hello!</em></strong>

이제 여기서 emphasize가 먼저 실행될지 strong이 먼저 실행될지 출력값을 보면 emphasize가 먼저 실행되는 것을 볼 수 있습니다. 따라서 데코레이터는 밑에서부터 위로 실행되는 것을 확인할 수 있습니다.

 

⭐️ 만약 @를 사용하지 않고 표현하고 싶으면 다음과 같이 표현하면 됩니다.

decorated_greet = strong(emphasis(greet))

 

인자를 받는 함수 장식하기

 

지금까지 모든 예제는 인자가 없는 '무인자' greet함수만 꾸몄습니다. 만약 인자를 취하는 함수를 작성할때는 파이썬의 가변적인 상황을 처리하는

*args

,

**kwargs

를 이용하면 편합니다.

먼저 proxy라는 데코레이터를 하나 생성하겠습니다.

def proxy(func):
    def wrapper(*args,**kwargs):
        return func(*args,**kwargs)
    return wrapper

 

wrapper 클로저 정의에서 *및 ** 연산자를 사용하여 모든 위치 및 키워드 인자를 수집하고 변수(args와 kwargs)에 저장합니다.

wrapper 클로저는 수집된 인자를 * 및 ** '인자풀기' 연산자를 사용하여 원래 입력 함수로 전달합니다.

 

이제 proxy 데코레이터에 의해 적용된 기술을 좀 더 유용한 실제 예제로 확장하겠습니다. 다음은 함수 인자와 결과를 기록하는

trace 데코레이터

입니다.

def trace(func):
    def wrapper(*args,**kwargs):
        print(f'TRACE : calling {func.__name__}() ' f'with {args}, {kwargs}')
        original_result = func(*args,**kwargs)
        print(f'TRACE : {func.__name__}() ' f'returned {original_result!r}')
        return original_result
    return wrapper

 

이제 trace로 함수를 장식한 다음 호출하면 장식된 함수에 전달된 인자들과 해당 반환값이 출력됩니다. 기초적인 예시지만 아래 처럼 입력하면 됩니다.

@trace
def say(name, line):
    return f'{name}:{line}'

# 출력 값
# TRACE : calling say() with ('Jane', 'Hello,World!'), {}
# TRACE : say() returned 'Jane:Hello,World!'
# Jane:Hello,World!

 

디버깅 가능한 데코레이터 작성법

 

데코레이터를 사용하면 실제로 한 함수가 다른 함수로 교체됩니다. 이 과정에서 단점은 원래(장식되지 않은) 함수에 첨부된 일부 메타데이터를 '숨겨' 버리는 것입니다.

 

예를 들면 원래 함수명, 독스트링(docstring), 매개 변수 리스트는 감싼 클로저에 의해 숨겨집니다. 예시는 다음과 같습니다.

def greet():
    """Return a friendly greeting."""
    return 'Hello'

def lowercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.lower()
        return modified_result
    return wrapper

decorate_greet = lowercase(greet)

이제 해당 함수 메타데이터에 접근을 하려고하면 대신 감싼 클로저의 메타데이터가 표시됩니다.

print(greet.__name__)
print(greet.__doc__)
print(decorate_greet.__name__)
print(decorate_greet.__doc__)

# 출력 값
# greet
# Return a friendly greeting.
# wrapper
# None

이것 때문에 파이썬 인터프리터로 디버깅하기가 불편하고 어려워 집니다. 하지만 이러한 문제는 functools.wrap을 통해 해결할 수 있습니다.

 

이것을 사용하면 잃어버린 메타데이터를 장식되지 않은 함수에서 데코레이터 클로저로 복사할 수 있습니다.

import functools
def uppercase(func):
    @functools.wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

@uppercase
def greet():
    """Return a freindly greeting."""
    return 'Hello!'

print(greet.__name__)
print(greet.__doc__)
# 출력 값
# greet
# Return a freindly greeting.

이런식으로 functools를 import한 후 functools.wraps(func)를 이용하면 디버깅할 때 편한것을 느낄 수 있습니다.

'Python > Python 개념' 카테고리의 다른 글

얕은 복사와 깊은 복사  (0) 2020.09.16
*args와 **kwargs  (0) 2020.09.14
파이썬 문자열 다루기  (1) 2020.05.25
파이썬 - list comprehension  (0) 2020.05.24
파이썬 - 모듈 Import  (1) 2020.03.30

댓글