본문 바로가기
프로그래밍 언어/Python

[Python] 데코레이터

by 토마토베이컨수프 2021. 11. 21.

데코레이터란?

데코레이터는 각 함수나 클래스를 둘러싼 후에 둘러쌓인 함수나 클래스에 대해 추가적인 기능을 수행하는 역할을 하는 도구입니다.

 

간단한 예로 문자열을 전달받아 대문자로 변환하는 함수가 있다고 합시다.

def to_upper_case(func):
    text = func()
    if not isinstance(text, str):
        raise TypeError("Not a string")
    return text.upper()

def say():
    return "welcome"
    
to_upper_case(say)	# WELCOME

 

to_upper_case함수는 문자열 대신 함수를 인자로 받아 해당 함수를 호출해 문자열을 가져와 텍스트를 대문자로 변환합니다. 위 코드를 다음과 같이 작성할 수 있습니다.

@to_upper_case
def hello():
    print("test")
    return "hello"	# test
    
hello	# HELLO

앞에 @를 붙여 to_upper_case함수를 데코레이터 함수로 만들어준 것입니다.

 

to_upper_case함수 아래에 다른 함수 wrapper를 정의해 upper()작업을 수행하게 하면 코드를 바로 실행하지 않고 함수의 동작을 변경할 수 있게 됩니다. 이제 함수가 실행되기 전과 함수가 실행을 완료한 후에 여러 작업을 수행할 수 있습니다. 위 코드와의 미묘한 차이를 확인해 보세요!

def to_upper_case(func):
    def wrapper():
        text = func()
        if not isinstance(text, str):
            raise TypeError("Not a string")
        return text.upper()
    return wrapper
    
@to_upper_case
def hello():
    print("test")
    return "hello"
    
hello()		# test HELLO

이처럼 함수 내에 내부 함수를 구현하고 그 내부 함수를 리턴하는 함수를 클로저(Closure)라고 합니다.

 

다른 예시로 함수가 작동되는데 걸리는 시간을 측정해주는 데코레이터 함수를 작성해봅시다.

def time_used(func):
    def wrapper():
        start = time.time()
        res = func()
        print(time.time() - start, "초 걸렸습니다")
        return res
    return wrapper
    
@time_used
def test():
    print("함수가 실행됩니다")
    time.sleep(2)
    
test()	# 함수가 실행됩니다	2.00370717048645 초 걸렸습니다

 


다중 데코레이터

다수의 데코레이터를 한 함수에 적용할 수도 있습니다.

문자열을 대문자로 바꿔주는 데코레이터 말고 문자열 앞에 접두사를 추가해주는 데코레이터를 만들고 두 데코레이터를 동시에 사용해봅시다.

def to_upper_case(func):
    def wrapper():
        text = func()
        if not isinstance(text, str):
            raise TypeError("Not a string")
        return text.upper()
    return wrapper
    
def add_prefix(func):
    def wrapper():
        text = func()
        result = " ".join(["Hello,", text])
        return result
    return wrapper

@add_prefix
@to_upper_case
def call_name():
    return "km"
    
call_name()	# Hello, KM

데코레이터는 아래에서 위로 적용되므로 to_upper_case가 먼저 호출된 다음 add_prefix가 호출되는 것을 확인할 수 있습니다.

 


데코레이터 인자

데코레이터 함수에 인자를 전달하여, 전달되는 함수의 인자의 갯수와 종류에 상관없이 동작하는 동적인 데코레이터를 만들 수 있습니다.

*args**kwargs를 사용하면 됩니다.

def to_upper_case(func):
    def wrapper(*args, **kwargs):
        text = func(*args, **kwargs)
        if not isinstance(text, str):
            raise TypeError("Not a string")
        return text.upper()
    return wrapper
    
@to_upper_case
def greeting(say):
    return say
    
greeting("hello")	# HELLO

 


상태 유지

함수에 데코레이터를 적용하면 그 함수의 __name__이나 __doc__값 같은 정보가 손상됩니다. 다음 예시를 한번 볼까요?

def logging(func):
    def logs(*args, **kwargs):
        """로그를 출력한다"""
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return logs
    
@logging
def foo(x):
    """제곱값을 반환한다"""
    return x*x
    
foo(2)	# 4
foo.__name__	# logs
foo.__doc__	# 로그를 출력한다

foo함수의 이름이나 독스트링이 아닌, 데코레이터 내부의 logs함수의 정보가 리턴되는 것을 볼 수 있습니다.

 

이를 해결하기 위해 functools.wraps를 사용할 수 있습니다.

from functools import wraps

def logging(func):
    @wraps(func)
    def logs(*args, **kwargs):
        """로그를 출력한다"""
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return logs
    
@logging
def foo(x):
    """제곱값을 반환한다"""
    return x*x
    
foo(2)	# 4
foo.__name__	# foo
foo.__doc__	# 제곱값을 반환한다

 

자, 그럼 지금까지 정리한 내용들을 종합해서 특정 url에 대해 정해진 횟수만큼 일정한 간격으로 요청을 반복하는 데코레이터 함수를 만들어 봅시다.

from functools import wraps
import time
import requests

def retry(retries=3, delay=10):
    def try_request(func):
        @wraps(func)
        def retry_decorators(*args, **kwargs):
            for retry in retries:
                func(*arg, **kwargs)
                time.sleep(delay)
        return retry_decorators
    return try_request
    
@retry(retries=4, delay=5)
def make_request(url, headers):
    try: 
        res = requests.get(url, headers)
        if res.status_code in (500, 502, 503, 429):
            raise Exception
    except Exception as e:
        raise FailedRequest("No connection")
    return res

 


클래스 데코레이터

클래스의 __call__메소드를 이용하면 클래스를 데코레이터로 사용할 수 있습니다.

__call__메소드에 대한 설명은 아래 포스트를 참고해주시길 바랍니다.

https://tomatobaconsoup.tistory.com/3

 

[Python] 매직 메소드를 통한 기능 구현

매직 매소드란? 클래스 안에 있는 특별한 메소드 이며 대표적인 예로 __init__, __str__ 메소드가 있습니다. a = 1 b = 2 print(a + b) # 3 그리고, 예를 들어 위 코드처럼 a 와 b 는 각각 1 과 2를 할당한 순간 a

tomatobaconsoup.tistory.com

 

아래 코드처럼 데코레이터 클래스를 구현할 수 있습니다. __call__메소드는 클래스를 데코레이트한 함수가 호출될 때마다 호출됩니다. functools 라이브러리는 클래스 데코레이터를 사용해 변수의 상태를 저장하는데 이용됩니다.

import functools

class Count:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num = 0
        
    def __call__(self, *args, **kwargs):
        self.num += 1
        print(f"Number of times called: {self.num}")
        return self.func(*args, **kwargs)
        
@Count
def hello():
    print("hello")
    
hello()	# Number of times called: 1	hello
hello()	# Number of times called: 2	hello

 

이번에는 타입 검사를 수행하는 클래스 데코레이터를 작성해 보겠습니다.

import functools

class ValidateParameters:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        
    def __call__(self, *parameters):
        if any([isinstance(item, int) for item in parameters]):
            raise TypeError("Parameter shouldn't be int")
        else:
            return self.func(*parameters)
            
@ValidateParameters
def add_numbers(*list_string):
    return "".join(list_string)
    
print(add_numbers("a", "n", "b"))	# anb
print(add_numbers("a", 1, "c"))		# TypeError: Parameter shouldn't be int

 


참고 자료

  • 파이썬 라이브러리 - 클로저와 데코레이터 : https://wikidocs.net/134789
  • <클린파이썬 : 효과적인 파이썬 코딩 기법, 수닐 카필 지음>

'프로그래밍 언어 > Python' 카테고리의 다른 글

[Python] 디버깅과 로그  (0) 2021.12.16
[Python] 비동기 프로그래밍  (0) 2021.12.07
[Python] 클래스 구조  (0) 2021.11.16
[Python] 제너레이터  (0) 2021.11.11
[Python] 유용한 데이터 구조  (0) 2021.11.07