1. 디스크립터 개요

1) 디스크립터 메커니즘

  • 디스크립터 : 디스크립터 프로토콜을 구현한 클래스의 인스턴스
  • 디스크립터 프로토콜
    : 아래 4개의 매직 메서드 중에 최소 한 개 이상을 포함해야 한다.
    __get__, __set__, __delete__, __set_name__

(1) 일반적인 클래스

class Attribute:
    value = 42


class Client:
    attribute = Attribute()


Client().attribute  # <__main__.Attribute object at 0x100ee8d90>
Client().attribute.value. # 42

(2) 디스크립터 작동 방식

디스크립터 구현에는 최소 2개의 클래스가 필요하다.

  • 클라이언트 클래스
  • 디스크립터 클래스
class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        print(f"{self.__class__.__name__}.__get__ 매직 메서드 호출({instance}, {owner}")
        return instance


class ClientClass:
    # 클래스 속성에 디스크립터 객체를 선언해야 한다.
    descriptor = DescriptorClass()


client = ClientClass()
client.descriptor  
# DescriptorClass.__get__ 매직 메서드 호출(<__main__.ClientClass object at 0x102f1feb0>, <class '__main__.ClientClass'>
<__main__.ClientClass object at 0x102f1feb0>

2) 디스크립터 프로토콜의 메서드 탐색

(1) get 메서드

  • 메서드 서명
def __get__(self, instance, owner):
  • 용어 정리
시그니처내용
self디스크립터 객체 자신
instance디스크립터를 호출한 객체
owner디스크립터를 호출한 객체의 클래스
  • owner 시그니처 존재 이유
    클라이언트 클래스가 직접 디스크립터를 호출하는 경우도 있기 때문이다.

  • 디스크립터 클래스에서 호출될 때와 디스크립터 객체어서 호출될 때를 비교해보자.

class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return f"{self.__class__.__name__}.{owner.__name__}"
        return f"{instance} 인스턴스"


class ClientClass:
    descriptor = DescriptorClass()

# 클래스에서 호출
ClientClass.descriptor  # DescriptorClass.ClientClass
# 객체에서 호출
ClientClass().descriptor  # '<__main__.ClientClass object at 0x104cd3eb0> 인스턴스'

(2) set 메서드

  • 메서드 서명
def __set__(self, instance, value):

디스크립터에 값을 할당할 때 호출된다.

  • 장점
    강력한 추상화 효과
  • 예시
from collections.abc import Callable
from typing import Any


class Validation:
    def __init__(self, validation_function: Callable[[Any], bool], error_msg:str) -> None:
        self.validation_function = validation_function
        self.error_msg = error_msg

    def __call__(self, value):
        if not self.validation_function(value):
            raise ValueError(f"{value!r} {self.error_msg}")


class Field:
    def __init__(self, *validations):
        self._name = None
        self.validations = validations

    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 validate(self, value):
        for validation in self.validations:
            validation(value)

    def __set__(self, instance, value):
        self.validate(value)
        instance.__dict__[self._name] = value


class ClientClass:
    descriptor = Field(
        Validation(lambda x: isinstance(x, (int, float)), "는 숫자가 아님"),
        Validation(lambda x: x>= 0, "는 0보다 작음"),
    )


client = ClientClass()
client.descriptor = 42
client.descriptor  # 42


client.descriptor = -42  
# ValueError: -42 는 0보다 작음


client.descriptor = "invalid value"
# ValueError: 'invalid value' 는 숫자가 아님

(3) delete 메서드

  • 메서드 서명
def __delete__(self, instance):
  • 예시
class ProtectedAttribute:
    def __init__(self, requires_role=None) -> None:
        self.permission_required = requires_role
        self._name = None

    def __set_name__(self, owner, name):
        self._name = name

    def __set__(self, user, value):
        if value is None:
            raise ValueError(f"{self._name}를 None으로 설정할 수 없음")
        user.__dict__[self._name] = value

    def __delete__(self, user):
        if self.permission_required in user.permissions:
            user.__dict__[self._name] = None
        else:
            raise ValueError(f"{user!s} 사용자는 {self.permission_required} 권한이 없음")


class User:
    """admin 권한을 가진 사용자만 이메일 주소를 삭제할 수 있음"""

    email = ProtectedAttribute(requires_role="admin")

    def __init__(self, username: str, email:str, permission_list: list = None) -> None:
        self.username = username
        self.email = email
        self.permissions = permission_list or []

    def __str__(self):
        return self.username


admin = User("root", "root@d.com", ["admin"])
user = User("user", "user1@d.com", ["email", "helpdesk"])

admin.email  # root@d.com
user.email  # user1@d.com

del admin.email

admin.email  # None
user.email  # user1@d.com

user.email = None  # ValueError: email를 None으로 설정할 수 없음

del user.email  # ValueError: user 사용자는 admin 권한이 없음

(4) set_name 메서드

  • 메서드 서명
def __set_name__(self, owner, name):

파이썬 3.6에서 추가된 매직 메서드다.
클라이언트 클래스와 디스크립터 이름을 파라미터로 받는다.

  • __set_name__ 도입 이전 방식
class DescriptorWithName:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, value):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value


class ClientClass:
    descriptor = DescriptorWithName("descriptor")


ClientClass().descriptor  # 'descriptor'
  • __set_name__ 도입 이후 방식
class DescriptorWithName:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, value):
        if instance is None:
            return self
        return instance.__dict__[self.name]


class ClientClass:
    descriptor = DescriptorWithName()  
    # descriptor = DescriptorWithName("descriptor") 도 가능


ClientCLass().descriptor  # 'descriptor'

2. 디스크립터 유형

1) 비데이터 디스크립터

  • 정의
    __get__ 매직 메서드만 구현한 디스크립터
  • 작동 방식
class NonDataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42


class ClientClass:
    descriptor = NonDataDescriptor()


client = ClientClass()
vars(client)  # {}

client.descriptor = 42
vars(client)  # {'descriptor': 42}

client.descriptor = 43
vars(client)  # {'descriptor': 43}

del client.descriptor
print(vars(client))  # {}

객체의 속성값으로 descriptor키를 찾을 수 있기에 디스크립터 프로토콜이 실행되지 않는다.
del을 호출해 객체의 속성을 초기화하면, 이제야 디스크립터 프로토콜이 실행된다.

2) 데이터 디스크립터

  • 정의
    __set__이나 __delete__메서드를 구현한 디스크립터

  • 작동 방식

class DataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42

    def __set__(self, instance, value):
        instance.__dict__["descriptor"] = value


class ClientClass:
    descriptor = DataDescriptor()


client = ClientClass()
client.descriptor  # 42
vars(client)  # {}

client.descriptor = 99
client.descriptor  # 42
vars(client)  # {'descriptor': 99}
client.__dict__["descriptor"]  # 99

이렇게 되는 이유는 데이터 스크립터의 경우, 속성을 조회하면 (객체의 __dict__에서 먼저 조회해보는 대신) 클래스의 descriptor를 먼저 조회하기 때문이다.

<=> 데이터 디스크립터는 인스턴스의 __dict__을 오버라이딩 한다.


3. 디스크립터 실전

  • 디스크립터로 처리할 수있는 몇 가지 상황
  • 디스크립터를 사용한 몇 가지 예, 여러 구현 방법과 각각의 장단점
  • 디스크립터 사용하기에 적합한 시나리오

1) 디스크립터를 사용한 애플리케이션

(1) 디스크립터를 사용하지 않을 때

from typing import List


class Traveler:

    def __init__(self, name: str, current_city: str) -> None:
        self.name = name
        self._current_city = current_city
        self._cities_visited = [current_city]

    @property
    def current_city(self) -> str:
        return self._current_city

    @current_city.setter
    def current_city(self, new_city: str) -> None:
        if self.current_city != new_city:
            self._cities_visited.append(new_city)
        self._current_city = new_city

    @property
    def cities_visited(self) -> List[str]:
        return self._cities_visited


>> alice = Traveler("Alice", "Korea")
>> alice.current_city  # Korea
>> alice.current_city = "Japan"
>> alice.current_city = "Germany"

>> alice.cities_visited  # ['Korea', 'Japan', 'Germany']

위의 코드는 잘 작동한다.
하지만 확장성이 없다.
구입한 모든 영화 티켓을 추적하고 싶거나
쇼핑몰에서 클릭했던 상품들을 추적하고 싶을 때마다
같은 로직을 반복해야 한다.

(2) 디스크립터 사용

class HistoryTracedAttribute:

    def __init__(self, trace_attribute_name: str) -> None:
        self.trace_attribute_name = trace_attribute_name
        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(instance, value):
            instance.__dict__[self.trace_attribute_name].append(value)

    def _needs_to_track(self, instance, value) -> bool:
        try:
            current_value = instance.__dict__[self._name]
        except KeyError:
            return True
        return value != current_value

    def _set_default(self, instance):
        instance.__dict__.setdefault(self.trace_attribute_name, [])


class Traveler:
    current_city = HistoryTracedAttribute("cities_visited")

    def __init__(self, name:str, current_city: str) -> None:
        self.name = name
        self.current_city = current_city
  • 포인트
    • (독립적/ 의존성 없음) 디스크립터에 비즈니스 로직이나 비즈니스 용어가 없다
    • 클라이언트에 의존성을 주입함으로써, 추적된 속성명과(current_city) 및 추적자명(cities_visited)에 대한 선택권이 클라이언트 단에 있다.

2) 디스크립터 잘못 구현했을 때와 솔루션

(1) 전역 상태 공유 이슈

임의의 클래스가 있을 때, 해당 클래스의 모든 인스턴스들은 클래스 속성을 공유한다.
따라서 각 객체에 데이터를 저장하도록 잘 설계해야 한다.

  • 잘못 설계한 예
class SharedDataDescriptor:

    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.value

    def __set__(self, instance, value):
        self.value = value


class ClientClass:
    descriptor = SharedDataDescriptor("첫 번째 값")


>> client1 = ClientClass()
>> client1.descriptor  # 첫 번째 값

>> client2 = ClientClass()
>> client2.descriptor  # 첫 번째 값

>> client2.descriptor = "client2에만 적용됐으면!"

>> client1.descriptor  # client2에만 적용됐으면!
>> client2.descriptor  # client2에만 적용됐으면!

ClientClass.descriptor가 고유하기 때문이다.(모든 인스턴스에 대해 동일한 속성)

따라서, 디스크립터는 각 인스턴스에 값을 보관하고 반환하도록 설계돼야 한다.

(2) 솔루션 - __dict__

(3) 솔루션 - 약한 참조

from weakref import WeakKeyDictionary


class DescriptorClass:

    def __init__(self, value):
        self.value = value
        self.mapping = WeakKeyDictionary()

    def _get__(self, instance, owner):
        if instance is None:
            return self
        return self.mapping.get(instance, self.value)

    def __set__(self, instance, value):
        self.mapping[instance] = value
  • 유의사항
    - 인스턴스는 더 이상 속성을 보유하지 않는다.
    • 인스턴스는 __hash__ 메서드를 구현해야 한다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN