__get__
, __set__
, __delete__
, __set_name__
class Attribute:
value = 42
class Client:
attribute = Attribute()
Client().attribute # <__main__.Attribute object at 0x100ee8d90>
Client().attribute.value. # 42
디스크립터 구현에는 최소 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>
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> 인스턴스'
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' 는 숫자가 아님
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 권한이 없음
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'
__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을 호출해 객체의 속성을 초기화하면, 이제야 디스크립터 프로토콜이 실행된다.
정의
__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__
을 오버라이딩 한다.
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']
위의 코드는 잘 작동한다.
하지만 확장성이 없다.
구입한 모든 영화 티켓을 추적하고 싶거나
쇼핑몰에서 클릭했던 상품들을 추적하고 싶을 때마다
같은 로직을 반복해야 한다.
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
임의의 클래스가 있을 때, 해당 클래스의 모든 인스턴스들은 클래스 속성을 공유한다.
따라서 각 객체에 데이터를 저장하도록 잘 설계해야 한다.
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가 고유하기 때문이다.(모든 인스턴스에 대해 동일한 속성)
따라서, 디스크립터는 각 인스턴스에 값을 보관하고 반환하도록 설계돼야 한다.
__dict__
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__
메서드를 구현해야 한다.