티스토리 뷰
Reference
- Fluent Python
- GeeksforGeeks
- Descriptor HowTo Guide(Raymond Hettinger)
- Stackoverflow
디스크립터(descriptor) - [1]편에 이어서 작성한다.
https://tcitr-antoliny.tistory.com/16
[1]편에는 디스크립터에 대한 기본적인 개념에 대해 설명했다면
[2]편에는 데이터 디스크립터와 비데이터 디스크립터 동작에 대한 차이점
그리고 디스크립터를 통해 만들어진 파이썬 키워드 property()에 대해 설명하겠다.
데이터 디스크립터, 비데이터 디스크립터
필자가 [1]편에서 디스크립터가 되는 조건(프로토콜)은
__get__, __set__, __delete__ 메서드 중 하나라도 구현하면
디스크립터처럼 동작한다고 설명했었다.
이중에서 __set__이 디스크립터에 구현되어 있냐 아니야에 따라
데이터 디스크립터와 비데이터 디스크립터로 나뉘는데
이런 __set__이하는 역할은
객체에 dot operation을 통해 해당 객체의 속성에 특정값을 설정하려고 할 때
만약 해당 객체의 속성이 디스크립터 객체일 경우
디스크립터 객체의 __set__메서드가 호출된다.
class Quantity:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = f'_{prefix}#{index}'
cls.__counter += 1
def __get__(self, instance, owner):
if instance == None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
coconuts = LineItem('Brazilian coconut', 20, 17.95)
※ LineItem의 weight, price속성은 디스크립터 객체가 관리한다.(클래스 변수를 보자)
LineItem객체를 만들 때 초기화 메서드인(__init__)을 통해서
>>> coconuts = LineItem('Brazilian coconut', 20, 17.95)
객체를 만들자마자 객체 속성에 값이 바로 초기화된다.
self.description --> 은 사실상 coconuts.description과 동일한데
위 코드를 보면 알 수 있듯이 description속성은 디스크립터가 관리하지 않는다.
그렇기 때문에 coconuts.__dict__에 해당 {속성: 값} --> {description: 'Brazilian coconut}
키와 값이 추가된다.
하지만 우리가 중점적으로 봐야 할 곳은 디스크립터 객체가 관리하는 속성 weight과 price이다.
위에서 __init__초기화 메서드를 통해 자동으로
coconuts.weight = 20이 적용되고
weight은 디스크립터 객체가 속성을 관리하므로
해당 디스크립터 클래스의 __set__메서드가 호출된다.
※ __set__의 매개변수
-----------------------------------------------
★ self: 디스크립터 객체 자기자신(Quantity) ★
★ instance: 호출한 객체(coconuts --> LineItem객체) ★
★ value: 설정하고자 하는 값(20) ★
-----------------------------------------------
해당 디스크립터의 __set__메서드를 보면 일단 무조건 값이 0보다 커야 하며
만약 크다면 setattr함수를 통해
호출한 객체의__dict__에 {속성: 값} --> {self.storage_name, value}
키와 값을 추가한다.
이런 디스크립터의 __set__메서드의 영향으로
coconuts.__dict__을 출력해보면
{'description': 'Brazilian coconut', '_Quantity#0': 20, '_Quantity#1': 17.95}
_Quantity#0과 _Quantity#1이라는 키와 값이 추가되어 있는걸 확인할 수 있다.(self.storage_name)
만약 디스크립터 객체가 해당 속성을 관리하지 않았다면
coconuts객체의 __dict__에는
{'description': 'Brazilian coconut', 'weight': 20, 'price': 17.95}
이런 키와 값이 존재할 것이다.
간단하게 요약하면
원래는 객체의 속성에 값을 추가하려고 하면
객체의 __dict__에 키와 값이 추가되는 동작을 거치지만
만약 해당 속성이 디스크립터 객체이면
원래 해야 할 동작을 해당 디스크립터의 __set__메서드가 가로채는 방식이 돼버린다.
이런 동작 때문에 디스크립터가 해당 객체의 속성을 관리한다라고 하는 것이다.
그런데 만약 __set__이 없고 __get__만 있다면 어떻게 될까??
class NondataDescriptor:
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
class Managed:
non_over = NondataDescriptor()
obj = Managed()
obj.non_over
>>> -> NondataDescriptor.__get__(<NondataDescriptor object>, <Managed object>, <class Managed>)
※ 참고로 print_args함수는 어떤 메서드가 호출되는지 그리고 매개변수에 어떤 값이 할당되었는지 알려주기 위한 메서드이다.
위 obj객체의 non_over속성은 NondataDescriptor가 관리한다.
non_over속성을 출력해보면
NondataDescriptor의 __get__메서드가 잘 호출된다는 걸 확인할 수 있다.
만약 non_over속성에 새로운 값을 대입하게 된다면
obj = Managed()
obj.non_over
>>> -> NondataDescriptor.__get__(<NondataDescriptor object>, <Managed object>, <class Managed>)
obj.non_over = 10
print(obj.non_over)
>>> 10
더 이상 디스크립터의 __get__이 호출되지 않는다.
사실 이러한 동작 부분은 필자가 이전에 썼던 [1]편에 dot operator의 동작원리 부분을
이해했다면 당연한 결과라는 걸 알 수 있다.
일단 필자가 위에서 설명했듯이 __set__메서드가 없기 때문에
__set__이 호출되지 않고
obj.__dict__에 {키: 값} --> {non_over: 10}
키와 값이 추가된다.
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
위 함수를 보면 알 수 있듯이
(CPython에서 C로 구현한 __getattribute__메서드를 파이썬 버전으로 해석한 함수)
비데이터 디스크립터보다 instance variable(객체.__dict__[속성])이 우선순위가 더 높다.
그렇기 때문에 더 이상 디스크립터가 호출되지 않고
객체.__dict__에 있는 값이 반환되는 것이다.
이러한 __getattribute__의 우선순위로 디스크립터가 가려지는 형태가 되어
비데이터 디스크립터를 '가릴 수 있는 디스크립터'라고도 부른다.
위와 같은 이유 때문에
우리가 꼭 읽기 전용 디스크립터를 만들어야 할 때는
__get__만 구현해야 하는 게 아니라 __set__도 구현해야 한다.
어떻게 보면 '가릴 수 있는 디스크립터'가 조금 문제가 있는 것처럼 보일 수 있지만
가릴 수 있다는 특성을 이용해 캐시 할 때 유용하게 사용할 수 있다.
결국 필자가 데이터 디스크립터와 비데이터 디스크립터 부분에서 말하고자 한 것은
__get__ or __set__둘 중 하나만 구현되어 있거나
둘 다 구현되어 있을 때
각각 다양한 결과를 도출할 수 있다는 것이다.
이러한 특성이 단점이라기 보단
장점으로 많이 응용되는 거 같다.
필자가 __set__에 대해 설명한 부분을 읽다 보면
@property의 getter & setter가 생각나지 않을까 싶다.
실제로 property() 내장 함수는
getter와 setter함수 객체를 디스크립터(property)의 인수로 보냄으로써 동작한다.
property
class LineItem:
def __init__(self, description, weight, price):
self.__description = description
self.__weight = weight
self.__price = price
def get_weight(self):
return self.__weight
def set_weight(self, weight):
if weight < 0:
raise ValueError
self.__weight = weight
Property(get_weight, set_weight) # 잠시 후 설명
def get_price(self):
return self.__price
def set_price(self, price):
if price < 0:
raise ValueError
self.__price = price
Property(get_price, set_price) # 잠시 후 설명
LineItem클래스를 보면 get_xx, set_xx 함수들이 있는 걸 확인할 수 있다.
해당 함수들은 LineItem객체의
특정 __속성(private)을 가져오거나 다른 값으로 바꿀 때 호출하는 함수이다.
보통 이런함수들을 getter & setter 함수(접근자)라고 부른다.
(JAVA를 아주 조금이라도 해본사람이면 알거라고 생각한다.)
★ 혹시나 getter & setter를 왜 사용하는지 궁금해할 수 있는 사람이 있기 때문에 남긴다. ★
1. 명확하게 표현이 가능 (coconuts.price = 10 --> coconuts.set_price(10))
2. 캡슐화(개발자들이 해당 속성을 쉽게 접근하지 못하게 만들기)
(하지만 파이썬에서는 완벽한 캡슐화를 지원 x)
3. readonly 구현
4. 무결성을 지키기 위함(위 set함수처럼 가격이나, 무게가 0미만이 될 수 x 설정)
이외에도 다양한 이유가 궁금하다면 이 글을 참고하기 바란다.
https://stackoverflow.com/questions/1568091/why-use-getters-and-setters-accessors
property() 함수는 해당 객체에 getter & setter가 있는지 확인한다.
property의 매개변수값으로 getter, setter함수 객체를 보내고
property는 그 함수를 기억한다.
그리고 실제로 내가 해당객체.getter()함수를 실행하면
먼저 디스크립터(property)의 __get__함수가 실행되면서
해당 속성의 getter함수가 있는지 확인하고
없다면 AttributeError를
있다면 .getter()함수를 실행하는 구조이다.
class Property:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget == None:
raise AttributeError(f'unreadable attribute {self._name}')
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"can't set attribute {self._name}")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"can't delete attribute {self._name}")
self.fdel(obj)
class LineItem:
def __init__(self, description, weight, price):
self.__description = description
self.__weight = weight
self.__price = price
def get_weight(self):
return self.__weight
def set_weight(self, weight):
if weight < 0:
raise ValueError
self.__weight = weight
Property(get_weight, set_weight)
def get_price(self):
return self.__price
def set_price(self, price):
if price < 0:
raise ValueError
self.__price = price
Property(get_price, set_price)
coconuts = LineItem('Brazilian coconut', 20, 17.95)
print(coconuts.get_price())
>>> 17.95
coconuts.set_price(20)
print(coconuts.get_price())
>>> 20
위 Property클래스는 내장함수 property()와 비슷하게 동작한다.
단지 property()함수가 어떻게 동작하는지 설명하기 위해서
Descriptor HowTo Guide문서에 있는 코드를 가져왔다.
하지만 실제로 우리가 Python에서 getter와 setter를 구현할 때는
@property 데코레이터를 사용하는 게 훨씬 더 코드가 깔끔하다.
property 외에도 @classmethod, @staticmethod 심지어
우리가 사용하는 사용자 정의 함수도 __get__이 구현된 비데이터 디스크립터다.
이러한 비데이터 디스크립터 특징을 이용해 클래스 안의 함수는 클래스에 바인딩된 메서드가 된다고 한다.
필자가 디스크립터에 대해 적었던 내용들은
레이몬드 헤팅거의 'Descriptor HowTo Guide'문서에서 대부분 가져온 것들이다.
(물론 Fluent Python내용도 많지만 책을 사야 하기 때문에..)
[1]편에서 'Descriptor HowTo Guide'문서를 추천해드리면서 마무리를 지었던 걸로 기억한다.
많은 분들이 디스크립터에 대한 이해도를 쌓길 바라는 마음으로
이번에도 필자는 'Descriptor HowTo Guide'의 링크를 남기고 떠나겠다.
https://docs.python.org/3/howto/descriptor.html#
후기
디스크립터와는 관련없는 내용이지만
8월부터 장고를 통해 아주 간단한 웹사이트를 만들 예정이라
최근에 자바스크립트를 배우기 시작했다.
예전에 처음 자바스크립트를 접했을 때
프로토타입 부분에서 그냥 포기했던 기억이 생각나는데
파이썬을 조금 하고 나서 다시 보니
이전보다는 이해하는 속도가 빨라진 느낌이 든다.
아직도 많이 부족하지만
그래도 조금 뿌듯하다.
후후
:)
'Python' 카테고리의 다른 글
[파이썬] 클래스 메타프로그래밍 (0) | 2022.07.30 |
---|---|
[파이썬]디스크립터(descriptor) - [1] (0) | 2022.07.21 |
[파이썬] yield from (0) | 2022.07.19 |
[파이썬] 코루틴 (0) | 2022.07.11 |
[파이썬] 데코레이터 (0) | 2022.07.04 |