개요
파이썬에서 디스크립터는 클래스를 통해 속성(attribute) 접근을 제어하기 위한 프로토콜(Protocol)입니다. 디스크립터는 클래스 내에 get, set, delete 메서드를 구현하여, 속성의 값을 읽거나 쓰거나 삭제하기 전에 추가적인 로직을 수행할 수 있도록 합니다.
이해를 위한 실생활 예시
비디오 가게 매니저 : 속성 접근을 관리하는 전문가
클래스 내의 속성을 마치 비디오 가게에 있는 영화라고 생각하면 이 영화에 대한 모든 접근(빌리기, 반납하기, 장부확인 등)을 직접 하는 게 아니라 그 역할을 전담하는 매니저를 두는 것이 바로 디스크립터의 역할입니다.
- 영화(movie) : 클래스의 속성
- 비디오 가게 매니저(manager) : 디스크립터
- 영화 빌리기 : __get__ 메서드 호출
- 손님이 영화를 달라고 요청하면 매니저는 현재 영화가 있는지, 이미 다른 사람이 빌려갔는지 확인하고 영화를 내줍니다.
- 영화 반납하기 : __set__ 메서드 호출
- 손님이 영화를 반납하면, 매니저는 영화 상태(손상 여부 등)를 확인하고, 반납 기록을 업데이트합니다.
- 영화 폐기하기 : __delete__ 메서드 호출
- 손님이 영화를 폐기해달라고 요청하면, 매니저는 해당 영화를 목록에서 제거하고 재고를 정리합니다.
이 매니저가 없다면, 손님은 직접 재고 목록을 뒤지고, 빌려간 사람을 추적하는 등 복잡한 과정을 거쳐야 합니다. 디스크립터 역시 이처럼 속성 접근에 대한 복잡한 로직을 캡슐화하여 사용자는 단순히 my_store.movie처럼 직관적으로 접근할 수 있게 해 줍니다.
아파트 경비원 : @property 데코레이터
@property는 아파트에 있는 경비원에 비유할 수 있습니다. 아파트의 특정 공간(parking_lot)에 접근하려면, 단순히 문을 열고 들어가는 것이 아니라 경비원을 거쳐야 합니다.
- _parking_lot(실제 주차장): 클래스의 비공개 속성, 직접 접근해서는 안 되는 내부 공간입니다.
- parking_lot(프런트 데스크) @properety로 정의된 속성, 사용자가 접근하는 공식적인 통로입니다.
- 차량 입차 : @parking_lot. setter
- 차량이 들어오면, 경비원은 차 번호를 확인하고, 빈자리가 있는 확인한 후 문을 열어줍니다.(__set__ 메서드)
- 차량 출처 : @parking_lot. deleter
- 차량이 나가면, 경비원은 기록을 남기고 문을 열어줍니다.(__delete__ 메서드)
이처럼 @property는 사용자가 주차장이라는 하나의 이름으로 접근하지만, 내부적으로는 입차, 출차, 조회 등 다양한 동작을 경비원(디스크립터)이 통제하게 만드는 구조입니다.
디스크립터의 기본 개념
디스크립터는 다른 객체의 속성으로 사용될 때 특별한 동작을 수행합니다. 예를 들어 obj.attr와 같은 속성 접근이 발생하면, 파이썬은 내부적으로 type(obj).__dict__['attr'].__get__(obj, type(obj))와 같이 디스크립터 메서드를 호출하여 속성 접근을 제어합니다. 이러한 동작 방식 덕분에, 디스크립터는 속성의 값을 가져오거나(__get__), 설정하거나(__set__), 삭제하는(__delete__) 과정을 사용자가 정의할 수 있게 해 줍니다.
디스크립터가 동작하는 세 가지 메서드
- __get__(self, instance, owner): 속성의 값을 가져올 때 호출됩니다.
- self: 디스크립터 객체 자체.
- instance: 속성에 접근하는 인스턴스 (예: obj).
- owner: 인스턴스의 클래스 (예: type(obj)).
- __set__(self, instance, value): 속성의 값을 설정할 때 호출됩니다.
- self: 디스크립터 객체 자체.
- instance: 속성에 값을 할당하는 인스턴스.
- value: 할당하려는 값.
- __delete__(self, instance): 속성을 삭제할 때 호출됩니다.
- self: 디스크립터 객체 자체.
- instance: 속성을 삭제하려는 인스턴스.
__set_name__ : 디스크립터의 유연성 향상
__set_name__(self, owner, name) 메서드는 디스크립터 인스턴스가 소유자 클래스에 처음 할당될 때 자동으로 호출됩니다. 이 메서드를 통해 디스크립터는 자신이 어떤 이름으로 정의되었는지 알 수. 있으며, 이 정보를 내부적으로 저장해 활용할 수 있습니다.
아래 코드에서 ManagedAttribute는 __set_name__ 덕분에 자신이 MyClass의 x속성에 할당되었는지, y 속성에 할당되었는지 스스로 인지하고 내부적으로 값을 관리할 수. 있습니다. 이를 통해 하나의 디스크립터 클래스로 여러 속성을 유연하게 제어할 수 있습니다.
class ManagedAttribute:
def __set_name__(self, owner, name):
"""디스크립터가 클래스에 할당될 때 호출됩니다."""
self.name = f"_{name}" # 내부적으로 값을 저장할 비공개 속성 이름 결정
def __get__(self, instance, owner):
print(f"속성 '{self.name[1:]}'의 값을 가져옵니다.")
if instance is None:
return self
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
print(f"속성 '{self.name[1:]}'에 '{value}' 값을 설정합니다.")
instance.__dict__[self.name] = value
class MyClass:
x = ManagedAttribute()
y = ManagedAttribute()
# 코드 실행
obj = MyClass()
obj.x = 100
obj.y = "Hello"
print(obj.x)
print(obj.y)
# 출력 결과
# 속성 'x'에 '100' 값을 설정합니다.
# 속성 'y'에 'Hello' 값을 설정합니다.
# 속성 'x'의 값을 가져옵니다.
# 100
# 속성 'y'의 값을 가져옵니다.
# Hello
디스크립터의 두 가지 유형
디스크립터는 __set__ 메서드의 존재 여부에 따라 두 가지 유형으로 나뉩니다.
데이터 디스크립터(Data Descriptor)
__get__ 과 __set__ 메서드를 모두 가지고 있습니다. 데이터 디스크립터는 인스턴스 __dict__에 있는 속성 보다 우선순위가 높습니다. 즉 인스턴스에 동일한 이름의 속성이 있더라도, 데이터 디스크립터의 __get__ 메서드가 먼저 호출됩니다.
비-데이터 디스크립터(Non-Data Descriptor)
__get__ 메서드만 가지고 있으며 __set__ 메서드가 없습니다. 비-데이터 디스크립터는 인스턴스 __dict__에 있는 속성보다 우선순위가 낮습니다. 만약 인스턴스에 동일한 이름의 속성이 있으면, 해당 속성이 먼저 반환되고 디스크립터는 무시됩니다. 파이썬의 메서드(method)는 이 비-데이터 디스크립터의 한 예입니다. 메서드는 클래스 레벨에서 정의되지만, 인스턴스를 통해 호출될 때 자동으로 첫 번째 인수로 인스턴스(self)를 바인딩합니다.
@property와 디스크립터
@property 데코레이터는 내부적으로 디스크립터를 활용하여 동작합니다. 이는 클래스의 속성 접근을 제어하는 파이썬의 대표적인 방법입니다.
@property의 작동원리
- @property는 실제로는 property 클래스의 인스턴스를 생성합니다.
- property 클래스는 __get__, __set__, __delete__ 메서드를 구현하는 데이터 디스크립터입니다.
- @property가 붙은 메서드는 property 클래스의 __get__ 메서드에 바인딩되어, 속성 값을 읽었을 때 호출됩니다.
- @proerty와 함께 사용되는 @name. setter 및 @name. deleter 데코레이터는 각각 property 객체의 __set__ 및 __delete__ 메서드에 다른 함수를 연결하는 역할을 합니다.
예시 코드
아래 코드에서 radius는 디스크립터입니다. c.radius는 property.__get__(c, Circle)를 호출하고, c.radius = 10은 property.__set__(c, 10)를 호출하여 유효성 검사를 수행합니다.
class Circle:
def __init__(self, radius):
self._radius = radius # 비공개 속성
@property # radius = property(get_radius)와 유사
def radius(self):
"""반지름 값을 가져옵니다."""
print("Getting radius")
return self._radius
@radius.setter
def radius(self, value):
"""반지름 값을 설정합니다."""
if value < 0:
raise ValueError("반지름은 음수가 될 수 없습니다.")
print("Setting radius")
메서드와 디스크립터
파이썬 클래스의 메서드는 비-데이터 디스크립터입니다. 클래스에 정의된 함수는 클래스의 속성으로 존재하며, 인스턴스를 통해 호출될 때 (instance.method()) 파이썬은 내부적으로 type(instance).__dict__['method'].__get__(instance, type(instance))와 같이 동작합니다. 이 과정에서 __get__ 메서드는 해당 함수를 인스턴스에 바인딩하여, 첫 번째 인수로 인스턴스 자체(self)를 전달해 줍니다. 이것이 바로 메서드 호출 시 self 인수가 자동으로 전달되는 이유입니다.
데이터 디스크립터(Data Descriptor)
데이터 디스크립터는 __get__ 과 set 메서드를 모두 구현한 디스크립터입니다. 이 디스크립터를 사용하면 속성의 값을 읽거나 쓸 때마다 __get__과 set 메서드에 정의된 로직이 실행됩니다.
예시
Descriptor 클래스는 __get__과 set 메소드를 모두 구현하고 있으므로, 데이터 디스크립터입니다. MyClass 클래스 내에 x라는 속성을 정의할 때, Descriptor 클래스를 이용하여 x의 값을 읽고 쓸 수 있습니다.
class Descriptor:
def __get__(self, instance, owner):
print("속성의 값을 가져옵니다.")
return instance.__dict__[self.name]
def __set__(self, instance, value):
print("속성의 값을 설정합니다.")
instance.__dict__[self.name] = value
class MyClass:
x = Descriptor()
비데이터 디스크립터(Non-Data Descriptor)
비데이터 디스크립터는 get 메서드만을 구현한 디스크립터입니다. 이 디스크립터를 사용하면 속성의 값을 읽을 때마다 get 메서드에 정의된 로직이 시행됩니다. 하지만 속성의 값을 쓰거나 삭제할 때는 일반적인 속성(attribute)으로 처리됩니다.
아래 코드에서 Descriptor 클래스는 get 메소드만을 구현하고 있으므로 비데이터 디스크립터입니다. MyClass 클래스 내에 x라는 속성을 정의할 때, Descriptor 클래스를 이용하여 x의 값을 읽을 수 있습니다.
class Descriptor:
def __get__(self, instance, owner):
print("속성의 값을 가져옵니다.")
return instance.__dict__[self.name]
class MyClass:
x = Descriptor()
클래스 디스크립터(Class Descriptor)
클래스 디스크립터는 클래스 레벨에서 디스크립터를 정의하고 해당 클래스의 모든 인스턴스에서 공유하는 디스크립터입니다. 클래스 디스크립터를 사용하면 해당 클래스의 모든 인스턴스에서 속성의 값을 읽거나 쓸 때마다, 클래스 레벨에서 정의된 __get__과 set 메서드가 실행됩니다.
Descriptor 클래스는 __get__과 set 메서드를 모두 구현하고 있으므로, 데이터 디스크립터입니다. MyClass 클래스 내에 x라는 속성을 정의할 때, Descriptor 클래스를 이용하여 x의 값을 읽고 쓸 수 있습니다. MyOtherClass 클래스는 MyClass를 상속받기 때문에, MyClass에 정의된 x 속성을 그대로 사용할 수 있습니다. obj1과 obj2는 서로 다른 인스턴스이므로, 각각의 x 속성 값을 따로 설정할 수 있습니다. 하지만 MyClass와 MyOtherClass는 같은 클래스 디스크립터 x를 공유하므로, x의 값을 읽거나 쓸 때마다 Descriptor 클래스의 __get__과 set 메서드가 실행됩니다.
class Descriptor:
def __get__(self, instance, owner):
print("속성의 값을 가져옵니다.")
return instance.__dict__[self.name]
def __set__(self, instance, value):
print("속성의 값을 설정합니다.")
instance.__dict__[self.name] = value
class MyClass:
x = Descriptor()
class MyOtherClass(MyClass):
pass
obj1 = MyClass()
obj2 = MyOtherClass()
obj1.x = 10
obj2.x = 20
print(obj1.x)
print(obj2.x)
디스크립터의 장단점
장점
- 속성에 접근할 때 특정한 동작을 수행하도록 할 수 있습니다.
- 코드의 재사용성을 높일 수 있습니다.
- 객체의 속성에 대한 제어를 세밀하게 조정할 수 있습니다.
단점
- 코드의 가독성이 떨어질 수 있습니다.
- 디버깅이 어려워질 수 있습니다.
- 오버헤드가 발생할 수 있습니다.
결론
파이썬 디스크립터는 속성 접근을 제어하는 강력한 도구입니다. 디스크립터를 사용하면 객체의 속성에 대한 제어를 세밀하게 조정할 수 있으며, 코드의 재사용성을 높일 수 있습니다. 하지만 디스크립터를 사용할 때는 코드의 가독성과 디버깅의 어려움, 오버헤드 등을 고려해야 합니다. 디스크립터를 적절하게 활용하면 파이썬 코드를 더욱 유연하고 강력하게 만들 수 있습니다.
'Dev > Python' 카테고리의 다른 글
[Linux] Python 가상환경 생성 과정 정리 (0) | 2025.01.21 |
---|