디스크립터(Descriptor)
디스크립터(Descriptor)는 파이썬 객체 지향 프로그래밍에서 속성(attribute)을 제어하는 데 사용되는 강력한 도구이다. 디스크립터를 사용하면 클래스 속성의 접근, 설정, 삭제 동작을 커스터마이즈할 수 있다. 디스크립터는 get, set, __delete__라는 세 가지 특별한 메서드를 구현한 객체이다.
디스크립터에는 세 가지 유형이 있다:
- 데이터 디스크립터 (Data Descriptor): __get__과 set 메서드를 구현한다. 경우에 따라 delete 메서드도 구현할 수 있다.
- 비데이터 디스크립터 (Non-Data Descriptor): get 메서드만 구현한다.
디스크립터 메서드
- get(self, instance, owner): 속성의 값을 가져올 때 호출된다. instance는 속성을 소유한 인스턴스이고, owner는 속성을 소유한 클래스이다. 클래스에서 호출된 경우 instance는 None이 될 수 있다.
- set(self, instance, value): 속성에 값을 설정할 때 호출된다.
- delete(self, instance): 속성을 삭제할 때 호출된다.
class MyDescriptor:
def __get__(self, instance, owner):
return instance._value
def __set__(self, instance, value):
instance._value = value
def __delete__(self, instance):
del instance._value
class MyClass:
descriptor = MyDescriptor()
# 사용 예
obj = MyClass()
obj.descriptor = 10 # __set__ 호출
print(obj.descriptor) # __get__ 호출, 10 출력
del obj.descriptor # __delete__ 호출
디스크립터를 사용하는 이유
디스크립터는 여러 가지 유용한 기능을 제공하는데, 그 중 몇 가지는 다음과 같다:
- 속성 접근 제어: 속성의 값을 읽거나 쓸 때, 또는 삭제할 때 특정 로직을 추가할 수 있다.
- 유효성 검사: 속성에 값을 설정할 때 유효성 검사를 수행할 수 있다.
- 캐싱: 속성 값을 캐싱하여 성능을 향상시킬 수 있다.
활용 예
디스크립터는 파이썬의 다양한 라이브러리와 프레임워크에서 사용된다. 예를 들어, Django ORM에서는 데이터베이스 필드를 디스크립터로 구현하여 필드에 접근할 때 자동으로 데이터베이스에서 값을 읽어오거나 쓰도록 한다.
디스크립터는 파이썬 객체 지향 프로그래밍의 중요한 개념 중 하나로, 이를 잘 활용하면 코드의 유연성과 재사용성을 크게 높일 수 있다.
디스크립터를 이용한 LineItem 클래스
class Quantity:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError(f"{self.name} must be > 0")
def __delete__(self, instance):
raise AttributeError("Cannot delete attribute")
class LineItem:
quantity = Quantity("quantity")
price = Quantity("price")
def __init__(self, description, quantity, price):
self.description = description
self.quantity = quantity
self.price = price
def total(self):
return self.quantity * self.price
# 사용 예
item = LineItem("Apple", 10, 1.5)
print(item.total()) # 15.0 출력
# 유효성 검사
try:
item.quantity = -1 # ValueError 발생
except ValueError as e:
print(e) # "quantity must be > 0" 출력
- Quantity 디스크립터 클래스:
- init: 디스크립터의 이름을 저장한다.
- get: 인스턴스의 사전(dict)에서 해당 속성의 값을 가져온다.
- set: 값을 설정할 때 양수인지 확인하고, 그렇지 않으면 ValueError를 발생시킨다.
- delete: 속성을 삭제하려고 할 때 AttributeError를 발생시킨다.
- LineItem 클래스:
- quantity와 price 속성은 Quantity 디스크립터를 사용하여 관리된다.
- init: 설명, 수량, 가격을 초기화한다.
- total: 총 비용을 계산하여 반환한다.
이 예제에서는 Quantity 디스크립터를 사용하여 LineItem 클래스의 quantity와 price 속성에 유효성 검사를 추가했다. 이를 통해 양수가 아닌 값이 설정되지 않도록 하여 데이터의 무결성을 유지할 수 있다.
오버라이딩 디스크립터와 논오버라이딩 디스크립터
파이썬에서 디스크립터는 두 가지 주요 유형으로 나눌 수 있다: 오버라이딩 디스크립터(Overriding Descriptor)와 논오버라이딩 디스크립터(Non-Overriding Descriptor). 이 두 유형의 차이는 속성 접근 시 행동 방식에서 비롯된다.
오버라이딩 디스크립터 (Overriding Descriptor)
오버라이딩 디스크립터는 set 메서드를 구현한 디스크립터이다. 이는 인스턴스의 속성에 직접 접근하는 것이 아니라 디스크립터를 통해 접근하는 것을 강제한다. set 메서드를 구현하지 않은 디스크립터는 논오버라이딩 디스크립터로 간주된다.
특징:
- 클래스에 정의된 속성에 값을 설정할 때 set 메서드가 호출된다.
- 인스턴스의 속성 접근 시 디스크립터의 get 메서드가 호출된다.
class OverridingDescriptor:
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = OverridingDescriptor()
obj = MyClass()
obj.attr = 10 # __set__ 호출
print(obj.attr) # __get__ 호출, 10 출력
get()이 없는 오버라이딩 디스크립터
get() 메서드가 없는 오버라이딩 디스크립터는 속성 값을 읽을 때 기본 동작을 제공하지 않는 디스크립터를 의미한다. 이런 경우에는 디스크립터의 다른 메서드인 set()와 delete()가 여전히 동작하지만, 속성 값을 읽으려고 하면 기본적으로 클래스 또는 인스턴스의 __dict__에서 값을 검색하게 된다.
오버라이딩 디스크립터는 주로 속성 값을 설정하거나 삭제할 때 특별한 처리를 하기 위해 사용된다. 하지만 get() 메서드가 없으면 속성 값을 읽을 때 디스크립터가 관여하지 않는다.
class OverridingWithoutGet:
def __set__(self, instance, value):
print(f"Setting {value}")
instance.__dict__['attr'] = value
def __delete__(self, instance):
print("Deleting attribute")
del instance.__dict__['attr']
class MyClass:
attr = OverridingWithoutGet()
# 사용 예
obj = MyClass()
obj.attr = 10 # __set__ 호출, "Setting 10" 출력
print(obj.attr) # 인스턴스의 __dict__에서 직접 값 가져옴, 10 출력
del obj.attr # __delete__ 호출, "Deleting attribute" 출력
- OverridingWithoutGet 디스크립터 클래스:
- set: 속성 값을 설정할 때 호출됩니다. 값을 설정할 때 추가로 출력 메시지를 표시한다.
- delete: 속성을 삭제할 때 호출됩니다. 삭제할 때 추가로 출력 메시지를 표시한다.
- MyClass 클래스:
- attr 속성에 OverridingWithoutGet 디스크립터를 할당한다.
- 속성 설정 및 접근:
- obj.attr = 10을 호출하면 set 메서드가 호출되어 "Setting 10" 메시지가 출력되고, 값이 인스턴스의 __dict__에 저장된다.
- print(obj.attr)을 호출하면 디스크립터의 get 메서드가 없기 때문에 인스턴스의 __dict__에서 직접 값을 가져온다.
- del obj.attr을 호출하면 delete 메서드가 호출되어 "Deleting attribute" 메시지가 출력되고, 속성이 인스턴스의 __dict__에서 삭제된다.
- get() 메서드가 없는 오버라이딩 디스크립터는 속성 값을 읽을 때 기본 동작을 제공하지 않는다.
- 속성 값을 설정(set())하거나 삭제(delete())할 때만 특별한 처리를 할 수 있다.
- 속성 값을 읽을 때는 인스턴스의 __dict__에서 직접 값을 검색하게 된다.
이와 같은 디스크립터는 특정 상황에서 속성의 설정과 삭제 동작을 커스터마이즈할 필요가 있을 때 유용하게 사용할 수 있다.
논오버라이딩 디스크립터 (Non-Overriding Descriptor)
논오버라이딩 디스크립터는 set 메서드를 구현하지 않은 디스크립터이다. 이는 인스턴스의 속성에 직접 접근할 수 있으며, 디스크립터를 통해 접근하는 것이 강제되지 않는다.
특징:
- 클래스에 정의된 속성에 값을 설정할 때 인스턴스의 __dict__에 직접 값을 설정한다.
- 인스턴스의 속성 접근 시 디스크립터의 get 메서드가 호출된다.
- 인스턴스에 직접 설정된 속성이 있을 경우 디스크립터가 무시된다.
class NonOverridingDescriptor:
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, None)
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = NonOverridingDescriptor()
obj = MyClass()
obj.attr = 10 # 인스턴스의 __dict__에 직접 설정됨
print(obj.attr) # 인스턴스의 __dict__에서 직접 가져옴, 10 출력
# 디스크립터로 설정되지 않은 속성 접근 시 디스크립터의 __get__ 호출
del obj.__dict__['attr']
print(obj.attr) # __get__ 호출, None 출력
- 오버라이딩 디스크립터: set 메서드를 구현하여 인스턴스 속성에 직접 접근하는 것을 방지하고, 속성 접근을 디스크립터를 통해서만 가능하게 만든다.
- 논오버라이딩 디스크립터: set 메서드를 구현하지 않아서 인스턴스 속성에 직접 접근할 수 있으며, 인스턴스에 직접 설정된 속성이 있을 경우 디스크립터가 무시된다.
이 두 유형의 디스크립터는 파이썬의 객체 속성 관리와 커스터마이징에 매우 유용하며, 다양한 상황에서 속성의 접근 제어 및 유효성 검사를 구현하는 데 사용된다.
클레스 안에서 디스크립터 덮어쓰기
클래스 안에서 디스크립터 덮어쓰기(Override)는 클래스에 정의된 디스크립터 속성을 인스턴스 레벨에서 다른 값으로 덮어쓰는 것을 의미한다. 디스크립터는 클래스 레벨에서 정의되지만, 인스턴스 레벨에서 속성을 직접 설정하면 디스크립터의 동작이 무시될 수 있다. 이는 특히 논오버라이딩 디스크립터에서 자주 나타나는 현상이다.
디스크립터 덮어쓰기
class NonOverriding:
def __get__(self, instance, owner):
return "NonOverriding descriptor value"
class MyClass:
attr = NonOverriding()
# 사용 예
obj = MyClass()
print(obj.attr) # "NonOverriding descriptor value" 출력
# 인스턴스 레벨에서 속성 덮어쓰기
obj.attr = "Instance attribute value"
print(obj.attr) # "Instance attribute value" 출력
# 클래스 레벨에서 디스크립터 접근
print(MyClass.attr) # "NonOverriding descriptor value" 출력
- NonOverriding 디스크립터 클래스:
- get 메서드를 구현하여 속성 값을 반환한다.
- MyClass 클래스:
- attr 속성에 NonOverriding 디스크립터를 할당한다.
- 속성 접근 및 덮어쓰기:
- print(obj.attr)을 호출하면 NonOverriding 디스크립터의 get 메서드가 호출되어 "NonOverriding descriptor value"를 출력한다.
- obj.attr = "Instance attribute value"를 호출하면 인스턴스의 __dict__에 attr 속성이 설정되어 디스크립터를 덮어쓴다.
- 다시 print(obj.attr)을 호출하면 인스턴스의 __dict__에서 직접 값을 가져와 "Instance attribute value"를 출력한다.
- 클래스 레벨에서 print(MyClass.attr)을 호출하면 여전히 디스크립터의 get 메서드가 호출되어 "NonOverriding descriptor value"를 출력한다.
오버라이딩 디스크립터와 논오버라이딩 디스크립터의 차이
- 오버라이딩 디스크립터는 set 메서드를 구현하여 인스턴스 레벨에서 속성을 직접 설정해도 디스크립터의 동작을 강제한다.
- 논오버라이딩 디스크립터는 set 메서드를 구현하지 않아서 인스턴스 레벨에서 속성을 직접 설정하면 디스크립터가 무시된다.
오버라이딩 디스크립터
class Overriding:
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name, "Overriding descriptor default value")
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = Overriding()
# 사용 예
obj = MyClass()
print(obj.attr) # "Overriding descriptor default value" 출력
# 인스턴스 레벨에서 속성 설정
obj.attr = "Instance attribute value"
print(obj.attr) # "Instance attribute value" 출력
# 클래스 레벨에서 디스크립터 접근
print(MyClass.attr) # 디스크립터 객체 자체 출력
- Overriding 디스크립터 클래스:
- get, set, set_name 메서드를 구현하여 속성 값을 읽고 쓸 때 특별한 처리를 한다.
- MyClass 클래스:
- attr 속성에 Overriding 디스크립터를 할당한다.
- 속성 접근 및 설정:
- print(obj.attr)을 호출하면 디스크립터의 get 메서드가 호출되어 기본 값을 출력한다.
- obj.attr = "Instance attribute value"를 호출하면 디스크립터의 set 메서드가 호출되어 인스턴스의 __dict__에 값을 설정한다.
- 다시 print(obj.attr)을 호출하면 디스크립터의 get 메서드가 호출되어 설정된 값을 출력한다.
- 클래스 레벨에서 print(MyClass.attr)을 호출하면 여전히 디스크립터의 get 메서드가 호출되어 기본 값을 출력한다.
디스크립터 덮어쓰기는 인스턴스 레벨에서 디스크립터의 동작을 무시하거나 변경할 수 있는 중요한 개념이다. 이를 통해 클래스와 인스턴스의 속성 관리 방식을 유연하게 조절할 수 있다.
메서드는 디스크립터
파이썬에서 메서드는 실제로 디스크립터의 한 형태이다. 메서드의 동작 원리를 이해하기 위해서는 디스크립터가 어떻게 작동하는지 이해하는 것이 중요하다. 파이썬의 메서드는 클래스의 속성으로 정의되며, 이는 디스크립터 프로토콜에 따라 동작한다.
메서드 디스크립터
메서드는 get 메서드를 가진 디스크립터이다. 파이썬에서 함수는 디스크립터 객체로, 클래스 속성으로 정의되면 자동으로 메서드로 변환된다.
함수 객체의 디스크립터 메서드
파이썬 함수 객체는 두 가지 주요 디스크립터 메서드를 가지고 있다:
- get(self, instance, owner)
- call(self, *args, **kwargs)
get 메서드
이 메서드는 메서드가 인스턴스에서 호출될 때 바인딩된 메서드 객체를 반환한다. 이 바인딩된 메서드는 인스턴스와 메서드 자체를 결합하여 호출할 수 있게 한다.
class MyClass:
def method(self):
print("Method called")
# 사용 예
obj = MyClass()
obj.method()
위의 예제에서 obj.method()가 호출될 때, 실제로는 다음과 같은 일이 발생한다:
- MyClass.method.get(obj, MyClass)가 호출됩니다. 이는 바인딩된 메서드 객체를 반환한다.
- 반환된 바인딩된 메서드 객체는 obj를 첫 번째 인자로 받아서 호출된다.
call 메서드
이 메서드는 함수 객체가 호출될 때 실행되는 메서드이다. 일반적으로 메서드를 호출할 때 이 메서드가 실행된다.
바인딩 메서드와 언바인딩 메서드
- 바인딩 메서드 (Bound Method): 인스턴스에 바인딩된 메서드이다. instance.method 형태로 호출된다. 이 경우 메서드는 인스턴스를 첫 번째 인자로 받는다.
- 언바인딩 메서드 (Unbound Method): 파이썬 3에서는 더 이상 존재하지 않지만, 이전 버전에서는 클래스에서 직접 호출되는 메서드를 의미했다. 현재는 클래스에서 메서드를 직접 호출하면 함수 객체가 반환된다.
class MyClass:
def method(self):
print("Method called")
# 사용 예
obj = MyClass()
bound_method = obj.method
bound_method() # "Method called" 출력
# 언바인딩 메서드는 더 이상 존재하지 않지만, 클래스에서 직접 호출하면 함수 객체가 반환된다.
unbound_method = MyClass.method
unbound_method(obj) # "Method called" 출력
메서드 디스크립터의 동작
파이썬 클래스 내부의 메서드가 어떻게 디스크립터로 동작하는지 이해하기 위해 내부적으로 어떤 일이 벌어지는지 살펴본다.
- 클래스 정의 시: 클래스가 정의될 때, 클래스의 메서드는 함수 객체로 정의되고 클래스의 속성이 된다.
- 인스턴스에서 메서드 접근 시: 인스턴스에서 메서드에 접근할 때, 함수 객체의 get 메서드가 호출되어 바인딩된 메서드 객체를 반환한다. 이 바인딩된 메서드는 인스턴스와 함수 객체를 결합하여 호출할 수 있게 한다.
- 메서드 호출 시: 바인딩된 메서드 객체가 호출될 때, 이는 내부적으로 함수 객체의 call 메서드를 호출하여 실제 메서드 구현을 실행한다.
커스텀 디스크립터를 사용한 예제
디스크립터를 사용하여 커스텀 메서드 동작을 정의할 수도 있다.
class CustomMethod:
def __get__(self, instance, owner):
def wrapper(*args, **kwargs):
print("Before method call")
result = self.method(instance, *args, **kwargs)
print("After method call")
return result
return wrapper
def __set_name__(self, owner, name):
self.name = name
def method(self, instance, *args, **kwargs):
print("Custom method called")
class MyClass:
method = CustomMethod()
# 사용 예
obj = MyClass()
obj.method()
- 메서드 디스크립터는 파이썬에서 함수 객체가 클래스의 속성으로 정의될 때 자동으로 동작한다.
- get 메서드는 바인딩된 메서드 객체를 반환하여 인스턴스와 메서드를 결합한다.
- 바인딩 메서드는 인스턴스에 바인딩된 메서드로, 인스턴스를 첫 번째 인자로 받는다.
- 언바인딩 메서드는 더 이상 존재하지 않지만, 클래스에서 메서드를 직접 호출할 수 있다.
이러한 메커니즘을 통해 파이썬의 메서드는 유연하고 강력한 방식으로 동작하며, 디스크립터 프로토콜을 이해하면 커스텀 동작을 쉽게 정의할 수 있다.
디스크립터 사용
디스크립터를 사용하는 것은 파이썬 객체 지향 프로그래밍에서 매우 강력한 도구이다. 올바르게 사용하면 객체의 속성 접근, 설정, 삭제를 세밀하게 제어할 수 있다. 다음은 디스크립터 사용에 대한 몇 가지 조언과 모범 사례이다:
1. 디스크립터 이해하기
디스크립터는 get, set, delete 메서드를 통해 속성 접근을 제어한다.
- get(self, instance, owner): 속성을 읽을 때 호출된다.
- set(self, instance, value): 속성을 설정할 때 호출된다.
- delete(self, instance): 속성을 삭제할 때 호출된다.
2. 데이터 디스크립터와 논데이터 디스크립터 구분하기
- 데이터 디스크립터: __get__과 __set__을 구현한 디스크립터이다. 인스턴스의 __dict__에 직접 접근하는 것을 막는다.
- 논데이터 디스크립터: __get__만 구현한 디스크립터이다. 인스턴스의 __dict__에 직접 접근을 허용한다.
3. 디스크립터의 활용 사례
- 유효성 검사: 디스크립터를 사용하여 속성 값 설정 시 유효성 검사를 수행할 수 있다.
- 속성 접근 제어: 속성 접근 시 로그를 남기거나 추가 동작을 수행할 수 있다.
- 캐싱: 계산 비용이 큰 속성 값을 캐싱하여 성능을 향상시킬 수 있다.
4. 실용적인 예제
유효성 검사 디스크립터
class Positive:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError(f"{self.name} must be > 0")
def __delete__(self, instance):
raise AttributeError("Cannot delete attribute")
class Product:
price = Positive("price")
def __init__(self, price):
self.price = price
# 사용 예
item = Product(10)
print(item.price) # 10 출력
try:
item.price = -5 # ValueError 발생
except ValueError as e:
print(e) # "price must be > 0" 출력
속성 접근 로그 디스크립터
class LoggedAccess:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
value = instance.__dict__[self.name]
print(f"Accessing {self.name}, value = {value}")
return value
def __set__(self, instance, value):
print(f"Updating {self.name} to {value}")
instance.__dict__[self.name] = value
def __delete__(self, instance):
print(f"Deleting {self.name}")
del instance.__dict__[self.name]
class Person:
age = LoggedAccess("age")
def __init__(self, age):
self.age = age
# 사용 예
p = Person(30)
print(p.age) # 로그 출력 후 30 출력
p.age = 35 # 로그 출력
del p.age # 로그 출력
'''
Updating age to 30
Accessing age, value = 30
30
Updating age to 35
Deleting age
'''
5. 복잡한 디스크립터 사용 피하기
디스크립터는 강력하지만, 코드의 복잡성을 높일 수 있다. 필요 이상의 복잡한 디스크립터 사용은 피하는 것이 좋다. 코드가 이해하기 어렵고 유지보수하기 힘들어질 수 있다.
6. 디스크립터와 메타클래스의 조합
디스크립터와 메타클래스를 조합하여 더 강력한 기능을 구현할 수 있다. 하지만 이는 매우 복잡할 수 있으므로 필요할 때 신중하게 사용해야 한다.
7. 디스크립터 사용 시 주의사항
- 이해하기 쉬운 코드 작성: 디스크립터를 사용하는 이유와 목적을 명확히 하고, 코드를 이해하기 쉽게 작성해야한다.
- 테스트와 문서화: 디스크립터는 일반 속성보다 복잡하므로 충분한 테스트와 문서화가 필요하다.
- 디스크립터 이름 충돌 피하기: 디스크립터 내부에서 인스턴스의 속성 이름과 충돌하지 않도록 주의해야한다. 보통 set_name 메서드를 사용하여 디스크립터 이름을 설정한다.
디스크립터는 파이썬에서 속성의 접근을 세밀하게 제어할 수 있는 강력한 도구이다. 이를 잘 활용하면 객체의 속성에 대해 유효성 검사, 접근 제어, 캐싱 등의 다양한 기능을 구현할 수 있다. 하지만 코드의 복잡성을 높일 수 있으므로 필요할 때 신중하게 사용해야 한다.
디스크립터 사용에 대한 조언
1. 코드를 간결하게 작성하기 위해 프로퍼티를 사용하라
디스크립터는 매우 강력하지만, 간단한 속성 접근 제어나 유효성 검사에는 파이썬의 property 데코레이터를 사용하는 것이 더 간단하고 직관적일 수 있다. property 데코레이터는 디스크립터의 동작을 추상화하여 더 간결한 코드를 작성할 수 있게 한다.
property 데코레이터 사용
class MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
if new_value > 0:
self._value = new_value
else:
raise ValueError("Value must be > 0")
# 사용 예
obj = MyClass(10)
print(obj.value) # 10 출력
obj.value = 20
print(obj.value) # 20 출력
try:
obj.value = -5 # ValueError 발생
except ValueError as e:
print(e) # "Value must be > 0" 출력
2. 읽기 전용 디스크립터는 set()을 구현해야 한다.
읽기 전용 디스크립터를 만들려면 set() 메서드를 구현하여 속성을 설정할 때 오류를 발생시켜야 한다. 이를 통해 속성이 설정되지 않도록 할 수 있다.
읽기 전용 디스크립터
class ReadOnlyDescriptor:
def __init__(self, value):
self._value = value
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
raise AttributeError("This attribute is read-only")
class MyClass:
attr = ReadOnlyDescriptor(10)
# 사용 예
obj = MyClass()
print(obj.attr) # 10 출력
try:
obj.attr = 20 # AttributeError 발생
except AttributeError as e:
print(e) # "This attribute is read-only" 출력
3. 검증 디스크립터는 set()만 사용할 수 있다.
검증 디스크립터는 주로 값을 설정할 때 유효성 검사를 수행한다. 이런 경우 set() 메서드만을 구현하여 유효성 검사를 수행하고 값을 설정할 수 있다.
검증 디스크립터
class PositiveValue:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError(f"{self.name} must be > 0")
class MyClass:
value = PositiveValue("value")
# 사용 예
obj = MyClass()
obj.value = 10
print(obj.value) # 10 출력
try:
obj.value = -5 # ValueError 발생
except ValueError as e:
print(e) # "value must be > 0" 출력
4. 캐시는 get()에서만 효율적으로 구현할 수 있다.
캐싱 디스크립터는 get() 메서드에서만 캐싱을 효율적으로 구현할 수 있다. 속성 값이 처음 접근될 때 계산되고, 이후 접근에서는 캐시된 값을 반환하는 방식이다.
캐싱 디스크립터
class CachedProperty:
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, instance, owner):
if instance is None:
return self
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.func(instance)
return instance.__dict__[self.name]
class MyClass:
@CachedProperty
def expensive_computation(self):
print("Computing value...")
return 42
# 사용 예
obj = MyClass()
print(obj.expensive_computation) # "Computing value..." 출력 후 42 출력
print(obj.expensive_computation) # 42 출력 (캐시된 값 사용)
5. 특별 메서드 이외의 메서드는 객체 속성에 의해 가려질 수 있다.
디스크립터에서 get(), set(), delete()와 같은 특별 메서드를 제외한 다른 메서드는 인스턴스 속성에 의해 가려질 수 있다. 이는 디스크립터가 아닌 일반 속성이 우선되기 때문이다. 따라서 디스크립터의 메서드를 호출하려면 특별 메서드를 사용하거나 클래스 레벨에서 직접 접근해야 한다.
디스크립터 메서드 가려짐
class Descriptor:
def __get__(self, instance, owner):
return self
def my_method(self):
return "Descriptor method"
class MyClass:
attr = Descriptor()
# 사용 예
obj = MyClass()
print(obj.attr.my_method()) # "Descriptor method" 출력 (정상 동작)
# 인스턴스 레벨에서 속성 설정
obj.attr = "Instance value"
print(obj.attr) # "Instance value" 출력 (디스크립터 가려짐)
# 디스크립터의 메서드 호출 시도
try:
print(obj.attr.my_method()) # AttributeError 발생
except AttributeError as e:
print(e) # "'str' object has no attribute 'my_method'" 출력
# 클래스 레벨에서 디스크립터 접근
print(MyClass.attr.my_method()) # "Descriptor method" 출력 (클래스 레벨 접근)
디스크립터는 파이썬에서 매우 유용한 기능이지만, 항상 가장 간단하고 직관적인 도구를 사용하는 것이 좋다. property 데코레이터는 간단한 속성 접근 제어에 유용하며, 디스크립터는 더 복잡한 속성 관리가 필요할 때 사용된다. 디스크립터를 사용할 때는 위의 조언을 참고하여 적절히 활용하는 것이 좋다.
디스크립터의 문서화 문자열과 관리 대상 속성의 삭제
디스크립터를 사용할 때, 문서화 문자열과 관리 대상 속성의 삭제를 처리하는 방법에 대해 알아보겠다.
디스크립터의 문서화 문자열
디스크립터에 대한 문서화 문자열은 doc 속성을 사용하여 설정할 수 있다. 이를 통해 디스크립터가 사용되는 클래스나 인스턴스에서 도움말 기능을 사용할 때 디스크립터에 대한 설명을 제공할 수 있다.
디스크립터에 문서화 문자열 추가하기
class DescriptorWithDoc:
"""This is a descriptor with a docstring."""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class MyClass:
attr = DescriptorWithDoc("attr")
# 사용 예
help(MyClass.attr) # 디스크립터의 __doc__ 문자열 출력
위의 예제에서, DescriptorWithDoc 클래스에 doc 문자열이 정의되어 있다. help(MyClass.attr)를 호출하면 디스크립터의 문서화 문자열이 출력된다.
관리 대상 속성의 삭제
디스크립터를 사용하여 속성 삭제를 제어할 수 있다. delete 메서드를 구현하여 인스턴스의 속성을 삭제할 때 원하는 동작을 정의할 수 있다.
속성 삭제를 제어하는 디스크립터
class ManagedAttribute:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
print(f"Deleting {self.name}")
del instance.__dict__[self.name]
class MyClass:
attr = ManagedAttribute("attr")
# 사용 예
obj = MyClass()
obj.attr = 10
print(obj.attr) # 10 출력
del obj.attr # "Deleting attr" 출력
try:
print(obj.attr) # None 출력
except AttributeError as e:
print(e)
위의 예제에서 ManagedAttribute 디스크립터는 delete 메서드를 구현하여 속성을 삭제할 때 추가적인 동작을 정의한다. del obj.attr을 호출하면 디스크립터의 delete 메서드가 호출되어 "Deleting attr" 메시지가 출력되고, 속성이 인스턴스의 __dict__에서 삭제된다.
디스크립터의 set_name 메서드
디스크립터가 클래스에 할당될 때, 디스크립터의 이름을 알 수 있도록 set_name 메서드를 구현할 수 있다. 이를 통해 디스크립터의 이름과 관련된 추가 초기화 작업을 수행할 수 있다.
set_name 메서드 구현
class DescriptorWithSetName:
def __init__(self, name=None):
self.name = name
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class MyClass:
attr = DescriptorWithSetName()
# 사용 예
obj = MyClass()
obj.attr = 10
print(obj.attr) # 10 출력
del obj.attr
print(obj.attr) # None 출력
- 문서화 문자열: 디스크립터에 doc 속성을 추가하여 문서화 문자열을 제공할 수 있다.
- 속성 삭제 제어: 디스크립터의 delete 메서드를 구현하여 속성 삭제를 제어할 수 있다.
- set_name 메서드: 디스크립터가 클래스에 할당될 때 디스크립터의 이름을 설정할 수 있도록 set_name 메서드를 구현할 수 있다.
이러한 기능들을 활용하여 디스크립터를 보다 강력하고 유연하게 사용할 수 있다. 디스크립터의 문서화와 속성 관리 기능을 적절히 활용하면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있다.
모든 코드는 github에 저장되어 있습니다.
'Python study' 카테고리의 다른 글
클래스 메타프로그래밍 (0) | 2024.06.07 |
---|---|
property (0) | 2024.06.06 |
asynico를 이용한 동시성 (1) | 2024.06.06 |
Future를 이용한 동시성 (1) | 2024.06.04 |
코루틴 (coroutine) (1) | 2024.06.04 |