Python 디스크립터를 Get, Set, Delete 프로토콜을 통해 파헤치기
Wenhao Wang
Dev Intern · Leapcell

소개
Python의 세계에서 속성에 접근하거나 메서드를 호출하는 것처럼 겉보기에는 단순한 작업들 뒤에는 강력하지만 종종 과소평가되는 메커니즘, 즉 디스크립터 프로토콜이 숨어 있습니다. 디스크립터는 Python이 객체 속성을 처리하는 방식을 매우 동적이고 유연하게 만드는 침묵의 설계자입니다. @property
데코레이터가 어떻게 작동하는지, 또는 바인딩되지 않은 메서드와 바인딩된 메서드가 self
인수를 어떻게 관리하는지 궁금한 적이 있습니까? 그 답은 종종 디스크립터로 거슬러 올라갑니다. 이 프로토콜을 이해하는 것은 단순히 Python의 내부를 분석하는 것 이상입니다. 그것은 더 표현력이 풍부하고, 강력하며, Python스러운 코드를 작성하는 능력을 얻는 것입니다. 이 글에서는 기본 __get__
, __set__
, __delete__
메서드를 탐구하여 Python 디스크립터의 미스터리를 풀고, 그 원리, 구현 및 실제 응용 프로그램을 보여줄 것입니다.
디스크립터 이해하기
핵심 프로토콜에 뛰어들기 전에 디스크립터가 무엇인지 정의해 봅시다. Python에서 __get__
, __set__
또는 __delete__
메서드 중 하나를 구현하는 객체를 디스크립터라고 합니다. 이러한 메서드는 특별한데, 이러한 메서드를 가진 객체가 클래스 속성에 할당될 때 Python은 해당 클래스의 인스턴스에 대한 속성 접근을 디스크립터의 메서드로 위임하기 때문입니다. 이 위임은 디스크립터 프로토콜의 초석입니다.
디스크립터에는 두 가지 주요 유형이 있습니다.
- 데이터 디스크립터:
__get__
와__set__
을 모두 정의하는 객체입니다. 데이터를 읽고 쓸 수 있기 때문에 "데이터"라고 합니다. - 비데이터 디스크립터:
__get__
만 정의하는 객체입니다. 주로 검색에 사용되며 인스턴스 딕셔너리에서 속성 값을 직접 수정할 수 없습니다.
데이터 디스크립터와 비데이터 디스크립터의 구분은 속성 조회 순서에 영향을 미치므로 중요합니다. 데이터 디스크립터는 인스턴스 딕셔너리에 우선권을 가지는 반면, 비데이터 디스크립터는 인스턴스 딕셔너리에 의해 재정의될 수 있습니다.
이제 디스크립터 프로토콜의 세 가지 기둥인 __get__
, __set__
, __delete__
를 살펴보겠습니다.
__get__
메서드
__get__
메서드는 속성에 접근할 때 호출됩니다. 일반적으로 시그니처는 __get__(self, instance, owner)
이며, 여기서:
self
: 디스크립터 인스턴스 자체입니다.instance
: 속성에 접근한 클래스의 인스턴스입니다. 속성에 클래스에서 직접 접근하는 경우 (MyClass.attribute
등),instance
는None
이 됩니다.owner
: 디스크립터를 소유한 클래스 (예:MyClass
)입니다.
예제를 통해 설명해 보겠습니다.
class MyDescriptor: def __init__(self, value=None): self._value = value def __get__(self, instance, owner): if instance is None: print(f"클래스 '{owner.__name__}'에서 디스크립터에 직접 접근:") return self print(f"인스턴스 '{instance}'에서 속성 가져오기 (값: {self._value})") return self._value def __set__(self, instance, value): print(f"인스턴스 '{instance}'에 대한 속성 설정: '{value}'") self._value = value class MyClass: descriptor_attr = MyDescriptor(10) # 클래스에서 접근 print(MyClass.descriptor_attr) # 인스턴스를 생성하고 접근 obj = MyClass() print(obj.descriptor_attr) # 출력: # 클래스 'MyClass'에서 디스크립터에 직접 접근: # <__main__.MyDescriptor object at 0x...> # 클래스에서 접근하면 디스크립터 객체 자체가 반환됩니다. # 인스턴스 '<__main__.MyClass object at 0x...>'에서 속성 가져오기 (값: 10) # 10
MyClass.descriptor_attr
에 접근하면 instance
가 None
으로 __get__
가 호출되어 디스크립터 객체 자체가 반환됩니다. obj.descriptor_attr
에 접근하면 instance
가 obj
로 __get__
가 호출되어 디스크립터가 해당 인스턴스에 특정 값을 반환하거나 여기서와 같이 공통 값을 반환할 수 있습니다.
__set__
메서드
__set__
메서드는 속성에 값이 할당될 때 호출됩니다. 시그니처는 __set__(self, instance, value)
이며, 여기서:
self
: 디스크립터 인스턴스입니다.instance
: 속성이 설정된 클래스의 인스턴스입니다.value
: 속성에 할당되는 값입니다.
이전 예제를 계속 진행해 보겠습니다.
# ... (위와 동일한 MyDescriptor 및 MyClass 정의) ... obj = MyClass() print(f"초기 obj.descriptor_attr: {obj.descriptor_attr}") obj.descriptor_attr = 20 # MyDescriptor.__set__ 호출 print(f"업데이트된 obj.descriptor_attr: {obj.descriptor_attr}") # 출력: # 초기 obj.descriptor_attr: 10 # 인스턴스 '<__main__.MyClass object at 0x...>'에 대한 속성 설정: '20' # 업데이트된 obj.descriptor_attr: 20
여기서 obj.descriptor_attr = 20
이 실행되면 MyDescriptor
의 __set__
메서드가 호출되어 할당된 값을 가로채고 잠재적으로 수정하거나 유효성을 검사할 수 있습니다. 이것이 프로퍼티가 읽기/쓰기 제어를 구현하는 방식입니다.
__delete__
메서드
__delete__
메서드는 del
키워드를 사용하여 속성이 삭제될 때 호출됩니다. 시그니처는 __delete__(self, instance)
이며, 여기서:
self
: 디스크립터 인스턴스입니다.instance
: 속성이 삭제된 클래스의 인스턴스입니다.
__delete__
를 포함하도록 예제를 확장해 보겠습니다.
class MyDescriptor: def __init__(self, value=None): self._value = value self.data = {} # 인스턴스별 값을 저장 def __get__(self, instance, owner): if instance is None: return self return self.data.get(instance, self._value) # 인스턴스에 설정되지 않은 경우 기본값으로 대체 def __set__(self, instance, value): self.data[instance] = value print(f"인스턴스 '{instance}'에 대한 속성 설정: '{value}'") def __delete__(self, instance): if instance in self.data: del self.data[instance] print(f"인스턴스 '{instance}'에 대한 속성 삭제") else: print(f"인스턴스 '{instance}'에 대한 속성을 삭제할 수 없습니다. 속성을 찾을 수 없습니다.") class MyClass: descriptor_attr = MyDescriptor(10) obj = MyClass() print(f"초기 obj.descriptor_attr: {obj.descriptor_attr}") # __get__ 사용, 기본값으로 대체 obj.descriptor_attr = 20 # __set__ 사용 print(f"obj.descriptor_attr 설정 후: {obj.descriptor_attr}") # 인스턴스 저장소의 __get__ 사용 del obj.descriptor_attr # __delete__ 호출 print(f"삭제 후 obj.descriptor_attr 접근 시도: {obj.descriptor_attr}") # __get__ 사용, 기본값으로 대체 # 출력: # 초기 obj.descriptor_attr: 10 # 인스턴스 '<__main__.MyClass object at 0x...>'에 대한 속성 설정: '20' # obj.descriptor_attr 설정 후: 20 # 인스턴스 '<__main__.MyClass object at 0x...>'에 대한 속성 삭제 # 삭제 후 obj.descriptor_attr 접근 시도: 10
이 향상된 예제에서 MyDescriptor
는 data
를 사용하여 인스턴스별로 값을 저장하도록 만들었습니다. del obj.descriptor_attr
가 호출되면 MyDescriptor.__delete__
가 호출되어 속성을 인스턴스의 내부 저장소에서 제거하는 등 정리 작업을 수행할 수 있습니다. obj.descriptor_attr
를 다시 접근하면 인스턴스별 저장소가 사라졌기 때문에 디스크립터 초기화 중에 제공된 기본값으로 다시 대체됩니다.
실제 응용 프로그램
디스크립터는 단순한 학문적 호기심이 아닙니다. 그것은 많은 Python 기능이 작동하는 방식의 기초입니다.
-
@property
데코레이터:@property
데코레이터는 아마도 가장 흔한 사용 사례일 것입니다. 클래스 메서드를 속성으로 변환하여 구문을 변경하지 않고도 클래스 멤버에 대한 제어된 접근(getter, setter, deleter)을 제공합니다.class Circle: def __init__(self, radius): self._radius = radius @property # 이것은 디스크립터입니다! def radius(self): print("반지름 가져오기...") return self._radius @radius.setter # 이것도 디스크립터입니다! def radius(self, value): if value < 0: raise ValueError("반지름은 음수일 수 없습니다.") print(f"반지름을 {value}로 설정...") self._radius = value c = Circle(5) print(c.radius) c.radius = 10 # c.radius = -1 # setter 때문에 ValueError 발생
-
바인딩된 메서드 및 바인딩되지 않은 메서드: 클래스 내에 정의된 모든 함수는 디스크립터가 됩니다.
MyClass.method
에 접근하면 디스크립터 자체(Python 2에서는 바인딩되지 않은 메서드, Python 3에서는 일반 함수)입니다.obj.method
에 접근하면 디스크립터의__get__
메서드가 호출되어obj
를 함수(self
)의 첫 번째 인수로 바인딩하여 바인딩된 메서드를 생성합니다. -
ORM 필드: SQLAlchemy와 같은 객체 관계형 Mappers (ORM)는 종종 데이터베이스 필드를 나타내기 위해 디스크립터를 사용합니다. 이를 통해 속성 접근을 가로채고, 필요할 때 데이터베이스에서 데이터를 로드하고, 영속성을 위한 변경 사항을 추적할 수 있습니다.
-
유형 검증: 사용자 정의 디스크립터는 유형 검사 또는 값 제약 조건을 강제하여 객체 내의 데이터 무결성을 보장할 수 있습니다.
class Typed: def __init__(self, type_name, default=None): self.type_name = type_name self.default = default self.data = {} # 인스턴스별 값 저장 def __get__(self, instance, owner): if instance is None: return self return self.data.get(instance, self.default) def __set__(self, instance, value): if not isinstance(value, self.type_name): raise TypeError(f"{self.type_name.__name__} 예상, {type(value).__name__} 얻음") self.data[instance] = value class Person: name = Typed(str, "Anonymous") age = Typed(int, 0) p = Person() print(p.name) p.name = "Alice" p.age = 30 # p.age = "thirty" # TypeError 발생 print(f"{p.name}는 {p.age}살입니다.")
결론
__get__
, __set__
, __delete__
메서드로 구체화된 Python 디스크립터 프로토콜은 속성 접근을 사용자 정의하는 강력하고 우아한 방법을 제공합니다. 이러한 메서드가 클래스 및 인스턴스 속성 조회와 어떻게 상호 작용하는지 이해함으로써 개발자는 객체 동작에 대한 더 깊은 제어를 잠금 해제하여 강력하고 고도로 유연한 시스템을 구축할 수 있습니다. 디스크립터는 Python의 정교한 속성 관리를 지원하는 숨겨진 영웅이며, 많은 사랑받는 기능을 가능하게 합니다. 그것들은 Python의 확장 가능한 디자인에 대한 진정한 증거입니다.