오히려 좋아..

상황이 나쁘게만 흘러가는 것 같을 때 외쳐보자.. .

궁금한 마음으로 포트폴리오 보기

Language/Python

[Python] 파이썬 노답 삼형제(1) - 데코레이터

junha6316 2021. 7. 17. 22:36

바야흐로 대개발자시대다. 많은 사람들이 다양한 이유로 IT업계로 모이고 있고 나도 그들 중 하나이다. 입문하는 사람들 대부분은 파이썬을 통해 이 곳에 발을 들인다. 파이썬이 상당히 배우기 쉬운 언어 중에 하나이기 때문이다. 일단 동적 타입 언어고 특별히 빌드과정 없이 스크립트상에서 바로 실행되기 때문에 입문 언어로 많이 선택한다. 하지만 파이썬이 마냥 쉬운 언어인가? 그렇지만은 않다.

바야흐로 대 개발자 시대

파이썬을 학습하면서 몇몇 고비가 있는데 바로 데코레이터, 제너레이터, 디스크립터 형제들이다. 사실 문법 특별히 어려운 건 아니지만 도대체 어디에 써먹어야 할지 감이 안잡히는 그런 형제들이다. 

파이썬 노답 삼형제

오늘의 포스팅은 파이썬 노답 삼형제 중에서도 첫째인 데코레이터에 대해 알아보도록 하겠다.

데코레이터 바로 쓰기

데코레이터는 일종의 synthetic sugar로 코드를 더 간단하게 만드는데 도움을 주는 문법이다. 이렇게 이야기해도 코드 한번 보는 것만 못하다. (백문이불여일코, 百聞不如一CO)어떻게 사용되는지 아래 코드를 실행시켜보자.

def deco(func):
    print("I'm decorator")
    return func

@deco
def target_with_deco():
	print("I'm function")


def target():
    print("I'm function")
    
target_with_deco()
target =deco(target)
target()

이 코드의 실행 결과는 아래과 같다. 위 코드를 통해 데코레이터의 동작 방식에 대해 알 수 있는데 먼저 데코레이터는 @deco와 같이 '@'를 이용해 함수 바로위에 작성되며 해당 함수를 입력으로 받아 어떤 처리 후 함수를 다시 반환하는 역할을 한다.

그럼 이걸 어디에 써..?

이 질문이 나올 줄 알았다. 일단 위 예제의 경우 아래처럼 함수 내부에서 구현해도 전혀 문제가 되지 않는다.  그렇다면 왜 데코레이터를 사용해야하는 것인가? 

def target():
    print("I'm decorator")
    print("I'm function")

만약 함수 10억개 있고 이 모든 함수가 실행 될때마다 "I'm decorator"라는 문장을 출력하고 싶다면 어떨까? 코드가 10억줄이 넘을 것이다. 여기 만약 "I'm decorator"를 "I'm on the next level"로 바꾼다고 해보자 그렇다면 10억번의 수정이 필요할 것이다.  물론 극단적인 예이긴 하지만 이와 비슷한 경우가 당연히 있을 수 있다. 만약 이 부분을 데코레이터를 이용한다면 수정시 상당히 간단하게 해결되는 것을 알 수 있을 것이다. 정리해보면 데코레이터는 함수에서 반복적으로 나타나는 로직을 독립적으로 분리해 재사용성과 유지보수를 좋게하는 synthetic sugar이다.

이딴 예는 마음에 들지 않아..  좀더 실용적인 걸 들고와라

웹 서비스에서는 현재 접근한 클라이언트가 로그인 상태인지 아닌지에 따라 다른 페이지를 보여줘야하는 경우가 꽤 많은데 그걸 일일히 모든 앤드포인트에 구현하게 되면 클린하지 않은 코드가 된다. 이러한 문제를 해결하기 위해서 파이썬 웹 프레임워크의 한 종류인 django에서는 view 함수에 현재 유저가 로그인 상태인지 아닌지 판별하는 login_required라는 데코레이터가 있다. 아래와 같이 사용되고 로그인 상태로 접근하면 index.html을 보여주지만 그렇지 않다면 로그인 페이지를 띄운다.

@login_required
def test_view(request):
   return render(request, 'index.html')

login_required는 아래와 같이 구현되어 있다.

def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test( #user_passes_test는 아래에 구현되어 있다.
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator
    
    
def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request.user):
                return view_func(request, *args, **kwargs)
            path = request.build_absolute_uri()
            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
            # If the login url is the same scheme and net location then just
            # use the path as the "next" url.
            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
            current_scheme, current_netloc = urlparse(path)[:2]
            if ((not login_scheme or login_scheme == current_scheme) and
                    (not login_netloc or login_netloc == current_netloc)):
                path = request.get_full_path()
            from django.contrib.auth.views import redirect_to_login
            return redirect_to_login(
                path, resolved_login_url, redirect_field_name)
        return _wrapped_view
    return decorator

다양한 데코레이터

데코레이터는 비단 함수에만 사용할 수 있는 건 아니다. 클래스형태의 데코레이터도 가능하다. 아래의 예시는 Python Clean Code(2nd Mariano Anaya)에서 발췌했다. 예를 들어 아래와 같은 코드가 있다고 해보자. LoginEvent는 말 그대로 로그인 이벤트를 나타내는 클래스이고 LoginEventSerializer는 이 로그인 이벤트를 serialize메서드를 이용해 딕셔너리 형태로 반환하는 클래스이다. 

class LoginEventSerializer:
    def __init__(self, evnet):
        self.event = evnet

    def serialize(self) -> dict:
        return {
            'username' : self.event.username,
            "password" : "**reperecated**",
            "ip" : self.event.ip,
            "timestamp" : self.event.timestamp.strftime(
                "%Y-%m-%d %H:%M"
            )
        }

@dataclass
class LoginEvent:
    SERIALIZER = LoginEventSerializer
    username: str
    password: str
    ip: str
    timestamp: datetime

    def serialize(self) -> dict:
        return self.SERIALIZER(self).serialize()

이 코드에는 몇가지 문제를 갖고 있는데 아래와 같다.

  1. 너무 많은 클래스 : 이벤트 개수가 증가할 수 록 Serialization 클래스가 증가함 
  2. 유연하지 않음 : 만약 각 부분을 재사용한다고 헀을 때 반복적으로 여러 클래스를 호출해야한다.
  3. serialize() 메서드가 모든 이벤트 클래스에 있어야한다.

이를 해결하기 위해 아래와 같이 클래스 데코레이터를 이용해 리팩토링할 수 있다. 이제 이벤트 클래스마다 Serializer 클래스를 선언하지 않아도 데코레이터 형식으로 표시해서 사용할 수 있게 되었다.

from dataclasses import dataclass
from datetime import datetime

def hide_field(field) -> str:
    return "**redacted**"

def format_time(field_timestamp: datetime) -> str:
    return field_timestamp.strftime("%Y-%m-%d %H:%M")

def show_original(event_field):
    return event_field

class EventSerializer:
    def __init__(self, serialization_fields: dict) -> None:
        self.serialization_fields = serialization_fields
    def serialize(self, event) -> dict:
        return {
            field : transformation(getattr(event, field))
            for field, transformation
            in self.serialization_fields.items()
        }
    
class Serialization:
    """
    Serialization 클래스 데코레이터
    먼저 이 데코레이터가 생성될 때 데코레이트 된 클래스의 serializer에 EventSerializer 객체를 할당해준다.
    EventSerializer
    """
    def __init__(self, **transformations):
        self.serializer = EventSerializer(transformations)

    def __call__(self, event_class):
        def serialize_method(event_instance):
            return self.serializer.serialize(event_instance)

        event_class.serialize = serialize_method
        return event_class 

@Serialization(
    username = str.lower,
    password=hide_field,
    ip= show_original,
    timestamp = format_time,
)
@dataclass
class LoginEvent:
    username: str
    password: str
    ip: str
    timestamp: datetime
    

if __name__ == "__main__":
    event = LoginEvent(username='park', password="123", ip="123.456.789.123", timestamp=datetime.now())
    print(event.serialize())
    #{'username': 'park', 'password': '**redacted**', 'ip': '123.456.789.123', 'timestamp': '2021-08-02 15:16'}

뭔가 아직 해결하지 못한 이상한 점은 파일을 실행 시키면 먼저 LoginEvent를 인스턴스화 하는 시점에서 Serialization 클래스가 동작하는게 아니라 먼저 Serialization이 먼저 동작한 이후에 LoginEvent를 인스턴스화 한다. 내려가면서 필요한 걸 갖고 오는게 아니라 먼저 클래스를 메모리에 잡아둔 이후에 실행하는 것 같다.

꺼진 데코레이터도 다시 사용하자

데코레이터는 결국 재사용성과 유지보수를 위한 것이라고 이야기했다. 하지만 함수 데코레이터를 클래스 내부의 메서드에 적용시키려고 하면 작동하지 않는 것을 확인 할 수 있다. 이를 해결하기 위해선 디스크립터를 사용해야한다. 아직 디스크립터에 대해선 다루지 않았으므로  그냥 데코레이터의 재사용성을 높이려면 디스크립터를 사용해야된다 정도로 생각하고 넘어가도록 하자. 아래 예제 역시 Python Clean Code(2nd Mariano Anaya)에서 발췌했다.

from functools import wraps
from log import logger

class DBDriver:
    def __init__(self, dbstring: str)-> None:
        self.dbstring = dbstring

    def execute(self, query:str) -> str:
        return f"query {query} at {self.dbstring}"

@inject_db_driver
def run_query(driver):
    return driver.execute("test_function")

#클래스의 메서드에 적용하려면 에러 발생 
#이를 해결하려면 __get__ 메서드를 구현 해준다.

from functools import wraps
from types import MethodType

class inject_db_driver:

    def __init__(self, function) -> None:
        self.function = function
        wraps(self.function)(self)

    def __call__(self, dbstring):
        return self.function(DBDriver(dbstring))

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.__class__(MethodType(self.function, instance))

 

3줄 정리

1. 데코레이터는 일종의 synthetic sugar로 함수 내부에 반복적인 로직을 분리할 수 있다.

2. 데코레이터는 함수형이 가장 기본적인 형태이지만 클래스 형태 역시 존재한다.

3. 데코레이터의 재사용성을 높이기 위해선 디스크립터를 사용해야한다.

'Language > Python' 카테고리의 다른 글

[Python] WSGI란?  (0) 2022.08.16
[FactoryBoy] Trouble Shooting  (0) 2021.11.06
[Python] Thread-safe 한 자료구조  (0) 2021.04.29
[Python] @property 너 누구야? 후아유  (0) 2021.03.28
[Python] 정적 메소드 staticmethod, classmethod  (0) 2021.03.27