@property 내장 기능의 가장 큰 문제점은 재사용성이다.
@property가 데코레이션하는 메서드를 같은 클래스에 속하는 여러 애트리뷰트로 사용할 수는 없다.
그리고, 서로 무관한 클래스 사이에서 @property 데코레이터를 적용한 머세드를 재사용할 수도 없다.
예를 들어, 학생의 숙제 점수가 백분율 값인지 검증하고 싶다고 하자.
class Homework:
def __init__(self):
self._grade = 0
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
if not (0 <= value <= 100):
raise ValueError('점수는 0과 100 사이입니다')
self._grade = value
@property를 사용하면 이 클래스를 쉽게 사용할 수 있다.
galileo = Homework()
galileo.grade = 95
이 학생에게 시험 점수를 부여하고 싶다고 하자. 시험 과목은 여러 개고, 각 과목마다 별도의 점수가 부여된다.
class Exam:
def __init__(self):
self._writing_grade = 0
self._math_grade = 0
@staticmethod
def _check_grade(value):
if not (0 <= value <= 100):
raise ValueError('점수는 0과 100 사이 입니다.')
이런식으로 계속 확장하려면, 시험 과목을 이루는 각 부분마다 새로운 @property를 지정하고
관련 검증 메서드를 작성해야 하므로 금방 지겨워진다.
@property
def writing_grade(self):
return self._writing_grade
@writing_grade.setter
def writing_grade(self, value):
self._check_grade(value)
self._writing_grade = value
@property
def math_grade(self):
return self._math_grade
@math_grade.setter
def math_grade(self, value):
self._check_grade(value)
self._math_grade = value
이러한 접근 방법은 일방적이지도 않다.
숙제나 시험 성적 이외의 부분에 백분율 검증을 활용하고 싶다면
똑같은@property와 검증 대상_grade 세터 메서드를 번거롭게 다시 작성해야한다.
파이썬에서 적용할 수 있는 더 나은 방법은 디스크립터를 사용하는 것이다.
- 디스크립터 프로토콜은 파이썬 언어에서 애트리뷰트 접근을 해석하는 방법을 정의한다.
- 디스크립터 클래스는 __get__과 __set__ 메서드를 제공하고, 이 두 메서드를 사용하면 별다른 준비 코드 없이도
원하는 점수 검증 동작을 제공할 수 있다.
- 같은 로직을 한 클래스 안에 속한 여러 다른 애트리뷰트에 적용할 수 있으므로 디스크립터가 믹스인 보다 낫다.
다음 코드는 Grade의 인스턴스인 클래스 애트리뷰트가 들어있는 Exam 클래스를 정의한다.
- Grade 클래스는 다음과 같은 디스크립터 프로토콜을 구현한다.
class Grade:
def __get__(self, instance, instance_type):
...
def __set__(self, instance, value):
...
class Exam:
# 클래스 애트리뷰트
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
Exam 인스턴에 있는 이런 디스크립터 애트리뷰트에 대한 접근을 파이썬이 어떻게 처리하는지 이해하는 것이 중요하다.
exam = Exam()
exam.writing_grade = 40
다음과 같이 해석됨
Exam.__dict__['writing_grade'].__set__(exam, 40)
다음과 같이 프로퍼티를 읽으면
exam.writing_grade
다음과 같이 해석됨
Exam.__dict__['writing_grade'].__get__(exam, Exam)
이러한 동작을 이끌어내는 것은 object의 __getattribute__ 메서드 입니다.
즉, Exam 인스턴스에 writing_grade 라는 이름의 애트리뷰트가 없으면 파이썬은 Exam 클래스의 애트리뷰트를 대신 사용한다.
이 클래스의 애트리뷰트가 __get__과 __set__메서드가 정의된 객체라면, 파이썬은 디스크립터 프로토콜을 따라야한다고 결정한다.
한 Exam 인스턴스에 정의된 여러 애트리뷰트에 접근할 경우에는 예상대로 작동합니다.
class Exam:
# 클래스 애트리뷰트
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
first_exam.science_grade = 99
print('쓰기', first_exam.writing_grade)
print('과학', first_exam.science_grade)
>>>
쓰기 82
과학 99
하지만 여러 Exam 인스턴스 객체에 대해 애트리뷰트 접근을 시도하면 예기치 못한 동작을 볼 수 있습니다.
second_exam = Exam()
second_exam.writing_grade = 75
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 틀림')
>>>
두 번째 쓰기 점수 75 맞음
첫 번째 쓰기 점수 82 틀림
문제는 writing_grade 클래스 애트리뷰트로 한 Grade 인스턴스를 모든 Exam 인스턴스가 공유한다는 점입니다.
이를 해결하려면 Grade 클래스가 각각의 유일한 Exam 인스턴스에 대해 따로 값을 추적하게 해야합니다.
- 인스턴스별 상태를 딕셔너리에 저장하면 이런 구현이 가능합니다.
class Grade:
def __init__(self):
self._values = {}
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('점수는 0과 100 사이입니다')
self._values[instance] = value
이 구현은 간단하고 잘 동작하지만, 바로 메모리를 누수(leak)시킨다는 점 입니다.
- _values 딕셔너리는 프로그램이 실행되는 동안 __set__ 호출에 전달된 모든 Exam 인스턴스에 대한 참조를 저장하고 있음
- 이로 인해 인스턴스에 대한 참조 카운터가 절대로 0이 될 수 없고, 따라서 쓰레기 수집기(garbage collector)가 인스턴스 메모리를 결코 재활용하지 못함
이 문제를 해결하기 위해 파이썬 weakref 내장 모듈을 사용할 수 있습니다.
from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
self._values = WeakKeyDictionary
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('점수는 0과 100 사이입니다')
self._values[instance] = value
class Exam:
# 클래스 애트리뷰트
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 틀림')
>>>
첫 번째 쓰기 점수 82 맞음
두 번째 쓰기 점수 75 맞음
이Grade 디스크립터 구현을 사용하면 모든 코드가 원하는대로 작동한다고 하는데
class Grade 부분에서 에러 발생 ..
### 기억해야 할 내용 ###
- @property 메서드의 동작과 검증 기능을 재사용하고 싶다면 디스크립터 클래스를 만들라.
- 디스크립터 클래스를 만들 때는 메모리 누수를 방지하기 위해 WeakKeyDictionary를 사용하라.
- __getattribute__가 디스크립터 프로토콜을 사용해 애트리뷰트 값을 읽거나 설정하는 방식을 정확히 이해하라.
'파이썬 코딩의 기술' 카테고리의 다른 글
파이썬코딩의기술 - Betterway 48 : __init_subclass__를 사용해 하위 클래스를 검증하라 (0) | 2023.03.24 |
---|---|
파이썬코딩의기술 : Betterway 47 - 지연 계산 애트리뷰트가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라 (0) | 2023.03.23 |
파이썬코딩의기술-BW45 : 애트리뷰트를 리팩터링하는 대신 @property를 사용하라 (0) | 2023.03.21 |
파이썬코딩의기술-BW44: 세터와 게터 메서드 대신 평범한 애트리뷰트를 사용하라 (0) | 2023.03.20 |
파이썬코딩의기술 - BW51 : 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라 (0) | 2023.03.17 |