지난 포스팅에서 다룬 디스크립터에 대해서 간단한 예제 코드를 살펴볼 것이다.
지금부터 살펴볼 예제는 속성을 가진 일반적인 클래스인데 속성의 값이 달라질 때마다 추적하려고 한다. 처음 떠오르는 해법은 속성의 setter 메서드에서 값이 변결될 때 검사하여 리스트와 같은 내부 변수에 값을 저장하는 것이다.
이번 애플리케이션에서 사용하는 클래스는 여행자를 표현하며 현재 어느 도시에 있는지를 속성으로 가진다. 프로그램을 실행하면서 사용자가 방문한 모든 도시를 추적할 것이다. 다음 코드와 같이 구현할 수 있다.
class Traveler:
def __init__(self, name, current_city):
self.name = name
self._current_city = current_city
self._cities_visited = [current_city]
@property
def current_city(self):
"""현재 도시를 반환"""
return self._current_city
@current_city.setter
def current_city(self, new_city):
"""현재 도시를 변경하고 방문한 도시 리스트에 현재 도시를 추가"""
if new_city != self._current_city:
self._cities_visited.append(new_city)
self._current_city = new_city
@property
def cities_visited(self):
"""방문한 도시 리스트를 반환"""
return self._cities_visited
alice = Traveler("Alice", "Barcelona")
alice.current_city = "Paris" # 현재 도시랑 다르니까 paris 추가하고 현재 도시 변경
alice.current_city = "Seoul" # 현재 도시랑 다르니까 seoul 추가하고 현재 도시 변경
alice.cities_visited # ['Barcelona', 'Paris', 'Seoul']

이렇게 프로퍼티를 사용하는 것만으로 충분하다. 그러나 애플리케이션의 여러 곳에서 똑같은 로직을 사용한다면 어떻게 될까? 속성의 모든 변수를 추적하는 것이 보다 일반적인 문제라 가정해보자. 예를 들어 Alice가 구입한 모든 티켓을 추적한다거나 방문했던 모든 국가를 추적하는 등의 일을 하고 싶다면 모든 곳에서 같은 로직을 반복해야 할 것이다.
게다가 다른 클래스에서도 같은 로직을 사용하려 한다면 어떻게 될까? 코드를 반복하거나 데코레이터, 프로퍼티 빌더 또는 디스크립터 같은 것을 만들어야 할 것이다. 프로퍼티 빌더는 디스크립터의 보다 복잡한 버전으로 여기에서는 디스크립터를 대신 이용한다.
이제 모든 클래스에 적용할 수 있도록 디스크립터를 사용하여 이전 섹션의 문제를 해결하는 방법을 살펴볼 것이다. 이번 예제에서는 유사한 패턴이 3회 이상 반복되어야 추상화를 고려해야 한다는 원칙을 지키지 않지만, 실전에서 디스크립터 사용방법을 묘사하는데 도움이 된다.
만약 실질적인 코드 반복의 증거가 없거나 복잡성의 대가가 명확하지 않다면 굳이 디스크립터를 사용할 필요가 없다.
이제 속성에 대해 이름을 가진 일반적인 디스크립터를 만들 것이다. 이 디스크립터는 값이 달라질 경우 리스트에 저장하여 추적하는 기능을 가진다.
# 이상적인 구현 방법
class HistoryTracedAttribute:
def __init__(self, trace_attribute_name: str)-> None:
self.trace_attribute_name = trace_attribute_name
# ㄴ> 속성의 이름은 디스크립터에 할당된 변수 중 하나로 여기서는 current_city이다.
# 그리고 이에 대한 추적을 저장할 변수의 이름을 디스크립터에 전달한다.
# 이 예에서는 cities_visited라는 속성에 current_city의 모든 값을 추적하도록 지시한다.
self._name = None
def __set_name__(self, owner, name):
self._name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self._name]
def __set__(self, instance, value):
self._track_change_in_value_for_instance(instance, value)
instance.__dict__[self._name] = value
def _track_change_in_value_for_instance(self, instance, value):
self._set_default(instance)
# ㄴ> 디스크립터를 처음으로 호출할 때는 추적 값이 존재하지 않을 것이므로
# 나중에 추가할 수 있도록 비어있는 배열로 초기화한다.
if self._needs_to_track_change(instance, value):
instance.__dict__[self.trace_attribute_name].append(value)
def _needs_to_track_change(self, instance, value)->bool:
try:
current_value = instance.__dict__[self._name]
except KeyError:
# ㄴ> 처음 Traveler를 호출할 때는 방문지가 없으므로 인스턴스 딕셔너리에서 current_city의 키도 존재하지 않을 것
return True
return value != current_value # 새 값이 현재 설정된 값과 다른 경우에만 변경 사항 추적
def _set_default(self, instance):
instance.__dict__.setdefault(self, trace_attribute_name, [])
# ㄴ> Traveler의 __init__ 메서드에서 디스크립터가 이미 생성된 단계이다.
# 할당 명령은 2단계 값을 추적하기 위한 빈 리스트 만들기를 실행하고,
# 3단계를 실행하여 리스트에 값을 추가하고 나중에 검색하기 위한 키를 설정한다.
class Traveler:
current_city = HistoryTracedAttribute("cities_visited")
def __init__(self, name, current_city):
self.name = name
self.current_city = current_city
디스크립터의 근간을 이루는 생각은 결국 다른 속성에서 발생하는 변경 사항을 추적할 수 있는 새로운 속성을 만들자는 것이다.
디스크립터의 코드가 다소 복잡한 것은 사실이나, 클라이언트 클래스의 코드는 상당히 간단해졌다. 따라서 이 디스크립터를 여러 번 사용한다면 앞서 살펴본 것처럼 충분히 가치가 있을 것이다.
또한 디스크립터 안에서는 어떠한 비지니스 로직도 포함되어 있지 않기 때문에 완전히 다른 어떤 클래스에 적용하여도 같은 효과를 낼 것이다. 이것이 완전 파이썬스러운 디스크립터의 특징이다. 디스크립터는 비즈니스 로직의 구현보다는 라이브러리, 프레임워크 또는 내부 API를 정의하는데 적합하다.