[파이썬] 데코레이터(1) 기본 사용

대심이·2024년 1월 21일

파이썬 클린 코드

목록 보기
2/5
post-thumbnail

마리아노 아나야의 <파이썬 클린 코드> 에서 소개하는 데코레이터 내용을 기반으로 작성했다.

데코레이터란?

파이썬의 데코레이터는 함수를 변형할 때 사용한다.

예를 들면 아래처럼 original 함수를 modifier 함수로 감싸서 사용하려고 한다고 해보자.

def original(...):
	# ...
original = modifier(original)

단순히 작성하면 이렇게 되겠지만, 실제로 original을 modifier로 감싸기 전과 후의 동작이 달라지니 여기저기서 쓴다면 깨지기 쉬운 코드가 되고 만다.

이를 데코레이터로 리팩토링하면 아래와 같다.

@modifier
def original(...):
	#...

syntax sugar

syntax sugar란?
동일한 기능을 읽기 쉽게 또는 코드 길이를 짧게 쓸 수 있게 도와준다.
예를 들면 i = i+1i++로 줄여 쓸 수 있도록 언어차원에서 지원하는 식이다.

데코레이터는 syntax sugar의 일종이다. 위의 예제만으로는 아직 문법적 달콤함이 느껴지지 않는데, 아래 예제를 보다보면 좀더 이해하기 쉬울 것이다.

시작하기 전에 용어를 정리하고 넘어가자.

  • 데코레이터: 래핑하는 함수. 예제에서 modifier.
  • 데코레이팅된(decorated) 객체: 래핑된(wrapped) 객체로 예제에서 original.

함수 데코레이터

def trace(fn):
	@wraps(fn)
    def wrapped(*args, **kwargs):
        print("before", fn.__name__)
        result = fn(*args, **kwargs)
        print("after", fn.__name__)
        return result
    return wrapped
  • trace는 데코레이터로 래핑될 함수 fn을 입력으로 받는다.
  • wrapped는 함수 fn을 꾸며주는 역할을 한다. 함수 실행 전후로 로그를 출력하고 실행 결과를 반환한다.
  • 마지막으로 trace는 정의된 wrapped 함수를 반환한다.

@wraps는 이 함수가 데코레이팅하는 함수라는 것을 알려주는 데코레이터다.

다음 실행 예제를 보자.

@trace
def hello(name):
    print("Hello,", name)

hello("World")

@tracehello 함수 위에 붙여주면 끝이다. 이로써 원본 함수 실행시 전후에 로그가 출력된다.

# 실행 결과
before hello
Hello, World
after hello

다음 예제는 함수 실행 시간을 계산해주는 데코레이터다.

import time

def timeit(fn):
    def wrapped(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        end = time.time()
        print("Elapsed time:", end - start)
        return result
    return wrapped

@timeit
def bubble_sort(arr):
    for i in range(len(arr) - 1):
        for j in range(len(arr) - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

arr = [5, 4, 3, 2, 1]
print(bubble_sort(arr))

""" 실행 결과
Elapsed time: 0.0009999275207519531
[1, 2, 3, 4, 5]
"""

데코레이터의 핵심은 기능의 분리다. 핵심 기능을 분리하고, 부가 기능은 다른 곳에서도 재사용할 수 있도록 돕는다.

전후 로그를 출력하는 기능도 함수 실행 시간을 계산해주는 기능도 다른 함수 위에 @를 붙이는 것만으로 쉽게 재사용할 수 있게 되었다.

클래스 데코레이터

이전에는 데코레이터의 파라미터로 함수를 받았고, 이번에는 클래스를 받아서 어떻게 사용되는지 살펴볼 것이다.

예제의 목표는 다음과 같다.

  • 사용자의 로그인 이벤트를 출력하고자 한다.
  • 이때 사용자의 비밀번호는 다른 문자열로 대체한다.
  • 로그인 일시는 특정 포멧으로 처리하고 싶다.
@Serialization(...?)
class LoginEvent:
    def __init__(self, username: str, password: str, ip: str, timestamp: datetime):
        self.username = username
        self.password = password
        self.ip = ip
        self.timestamp = timestamp

event = LoginEvent("User", "PA$$W0RD", "127.0.0.1", datetime.now())
print(event.serialize())

래핑될 클래스이다. 클래스 위에 @Serialization을 달면 래핑될 클래스에 serialize 라는 함수가 추가된다. 그 다음 event.serialize()를 호출하면 passwordtimestamp 값이 변경된 값을 출력하게 될 것이다.

그렇다면, 실제 업무를 처리할 함수를 만들어보자.

def hide_field(field) -> str:
    return "**민감 정보 삭제**"

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

def show_original(event_field):
    return event_field

serialize() 내에서 위 함수들을 알뜰하게 호출해서 사용할 예정이고, @Serialization() 내부에는 다음과 같이 쓰면 좋을 것이다.

@Serialization(
    username=show_original,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
class LoginEvent:
	# ...

usernameip는 그대로, password는 숨김 처리, timestmp는 포메팅할 함수를 사용하고 싶다고 명시했다.

다음 데코레이터 클래스를 만드는데 두 개의 클래스로 나눌 것이다. 하나는 데코레이팅을 담당하는 Serialization, 다른 하나는 실제 함수를 호출하고 처리하는 EventSerializer이다.

class Serialization:
    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 클래스를 먼저 살펴보면, 생성자에는 @Serialization(...)의 인자로 넘어온 username=show_original, password=hide_field, ... 값들이 transformers에 dict 형태로 들어온다.

그런 다음 EventSerializer 클래스 객체를 생성하고 내부 파라미터로 둔다.

__call__의 인자로 클래스가 들어오고, 실제로 래핑 역할을 할 serialize_method를 정의한 다음 클래스에 메서드를 달아주는 작업을 한다.

다음 실제 serialize 작업을 진행할 EventSerializer를 살펴보자.

class EventSerializer:
    def __init__(self, serialization_fields: dict):
        self.serialization_fields = serialization_fields
    
    def serialize(self, event) -> dict:
        return {
            field: transformation(getattr(event, field))
            for field, transformation in self.serialization_fields.items()
        }

생성자의 serialization_fields에는 { username=show_original, password=hide_field, ... } 이 값들이 들어오게 될 것이다.

serializeevent로 LoginEvent 객체가 들어온다.

serialization_fields를 순회하면서 객체의 값들을 변경하는 작업이 일어나고 그 결과를 반환한다.

마지막으로 동작결과를 보면, 객체 내부 값들이 잘 변경되었음을 확인할 수 있다.

@Serialization(
    username=show_original,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
class LoginEvent:
    def __init__(self, username: str, password: str, ip: str, timestamp: datetime):
        self.username = username
        self.password = password
        self.ip = ip
        self.timestamp = timestamp

event = LoginEvent("User", "PA$$W0RD", "127.0.0.1", datetime.now())
print(event.serialize()) #=> {'username': 'User', 'password': '**민감 정보 삭제**', 'ip': '127.0.0.1', 'timestamp': '2024-01-21 18:21'}

책에 나온 이 예제는 데코레이터와 실제 처리 로직이 잘 분리되어서 다른 클래스에서도 재사용할 수 있게 설계된 것을 알 수 있다.

마지막으로 잘 쓰이는 클래스 데코레이터 한 가지만 더 소개하고 이 장을 마친다.

class LoginEvent:
    def __init__(self, username: str, password: str, ip: str, timestamp: datetime):
        self.username = username
        self.password = password
        self.ip = ip
        self.timestamp = timestamp

생성자에서 내부 파라미터를 할당하는 부분이 늘 같은 로직으로 반복된다. 이는 파이썬에서 기본적으로 제공하는 데코레이터로 쉽게 리팩토링할 수 있다.

from dataclasses import dataclass

@dataclass
class LoginEvent:
    username: str
    password: str
    ip: str
    timestamp: datetime

@dataclass 데코레이터를 붙여준 다음, 파라미터를 정의만 해두면 자동으로 생성자가 만들어진다.

맺으며

사용법에 대해서 배웠으니 다음에는 데코레이터가 주로 활용되는 영역과 주의사항에 대해서 작성하려고 한다.

위에 잠깐 나왔던 @wraps 데코레이터의 실제 역할과 고차함수적 특징 등을 다룰 예정이다.

다음 글 [파이썬] 데코레이터(2) 기본 사용에서 계속된다.

profile
대범해지고 싶어서 대심이

0개의 댓글