@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
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를 지정하고 관련 검증 메서드를 작성해야한다.
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
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
게다가 이런 접근 방법은 일방적이지도 않다.
더 나은 방법은 디스크립터를 사용하는 것이다.
디스크립터 프로토콜은 파이썬 언어에서 애트리뷰트 접근을 해석하는 방법을 정의한다.
디스크립터 클래스는 get__과 __set 메서드를 제공하고, 이 두 메서드를 사용하면 별다른 준비 코드 없이도 원하는 점수 검증 동작을 재사용할 수 있다.
이런 경우 같은 로직을 한 클래스 안에 속한 여러 다른 애트리뷰트에 적용할 수 있으므로 디스크립터가 믹스인보다 낫다.
class Grade:
def __init__(self):
self._value = 0
def __get__(self, instance, instance_type):
return self._value
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(
'점수는 0과 100 사이입니다')
self._value = value
class Exam:
# 클래스 애트리뷰트
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
exam = Exam()
exam.writing_grade = 40
Exam.__dict__['writing_grade'].__set__(exam, 40)
exam.writing_grade
40
Exam.__dict__['writing_grade'].__get__(exam, Exam)
40
이런 동작을 이끌어내는 것은 object의 getattribute 메서드다.
간단히 말해, Exam 인스턴스에 writing_grade라는 이름의 애트리뷰트가 없으면 파이썬은 Exam 클래스의 애트리뷰트를 대신 사용한다.
이 클래스의 애트리뷰트가 get__과 __set 메서드가 정의된 객체라면 파이썬은 디스크립터 프로토콜을 따라야한다고 결정한다.
first_exam = Exam()
first_exam.writing_grade = 82
first_exam.science_grade = 99
print('쓰기', first_exam.writing_grade)
print('과학', first_exam.science_grade)
second_exam = Exam()
second_exam.writing_grade = 75
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 틀림; '
f'82점이어야 함')
쓰기 82 과학 99 두 번째 쓰기 점수 75 맞음 첫 번째 쓰기 점수 75 틀림; 82점이어야 함
하지만 이 구현은 잘못되었다.
Eaxm 여러 객체에 대해 접근하면 위와 같이 나온다.
애트리뷰트로 한 Grade 인스턴스를 모든 Exam 인스턴스가 공유한다.
이를 해결하려면 Grade 클래스가 각각의 유일한 Exam 인스턴스에 대해 따로 값을 추적하게 해야한다.
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
그냥 dictionary로 할 시 메모리 리킹이 발생한다.
따라서 WeakKeyDictionary를 사용할 수 있다.
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'첫 번째 쓰기 점수 {first_exam.writing_grade} 맞음')
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
첫 번째 쓰기 점수 82 맞음 두 번째 쓰기 점수 75 맞음