마리아노 아나야의 <파이썬 클린 코드> 에서 소개하는 데코레이터 내용을 기반으로 작성했다.
파이썬의 데코레이터는 함수를 변형할 때 사용한다.
예를 들면 아래처럼 original 함수를 modifier 함수로 감싸서 사용하려고 한다고 해보자.
def original(...):
# ...
original = modifier(original)
단순히 작성하면 이렇게 되겠지만, 실제로 original을 modifier로 감싸기 전과 후의 동작이 달라지니 여기저기서 쓴다면 깨지기 쉬운 코드가 되고 만다.
이를 데코레이터로 리팩토링하면 아래와 같다.
@modifier
def original(...):
#...
syntax sugar란?
동일한 기능을 읽기 쉽게 또는 코드 길이를 짧게 쓸 수 있게 도와준다.
예를 들면i = i+1을i++로 줄여 쓸 수 있도록 언어차원에서 지원하는 식이다.
데코레이터는 syntax sugar의 일종이다. 위의 예제만으로는 아직 문법적 달콤함이 느껴지지 않는데, 아래 예제를 보다보면 좀더 이해하기 쉬울 것이다.
시작하기 전에 용어를 정리하고 넘어가자.
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")
@trace를 hello 함수 위에 붙여주면 끝이다. 이로써 원본 함수 실행시 전후에 로그가 출력된다.
# 실행 결과
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()를 호출하면 password와 timestamp 값이 변경된 값을 출력하게 될 것이다.
그렇다면, 실제 업무를 처리할 함수를 만들어보자.
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:
# ...
username과 ip는 그대로, 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, ... } 이 값들이 들어오게 될 것이다.
serialize내 event로 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) 기본 사용에서 계속된다.