"Better Way 29. 게터와 세터 메서드 대신에 일반 속성을 사용하자."에서 소개된 @property의 가장 큰 문제점은 재사용성이다.
다시 말해, @property로 데코레이트하는 메서드를 같은 클래스에 속한 여러 속성에 사용하지 못한다.
또한, 관련 없는 클래스에서도 재사용할 수 없다.
아래의 예시를 보자.
글쓰기(writing)와 수학(math)의 시험 점수를 관리하는 Exam이라는 클래스를 만든다고 해보자.
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('Grade must be between 0 and 100')
_check_grade 메소드를 이용해 각 시험 점수에 입력되는 내용을 검증하려고 한다면, 코드는 중복 코드로 인해 금방 장황해진다.
@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
이렇게 중복되는 코드를 없애주기 위해, 디스크립터(descriptor)를 사용할 수 있다.
아래의 예제를 보자.
class Grade:
def __get__(*args, **kwargs):
# ...
def __set__(*args, **kwargs):
# ...
class Exam:
math_grade = Grade()
writing_grade = Grade()
Grade 라는 클래스를 만들고, Exam의 각 속성을 Grade 클래스에 의해 값이 생성되고 관리되게 할 수 있다.
이 때, 속성으로 이용되는 클래스(Grade)에 __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('Grade must be between 0 and 100')
self._value = value
그런데, 이렇게만 하면 한가지 문제가 발생한다.
한 Grade 인스턴스가 모든 Exam 인스턴스의 writing_grade 클래스 속성으로 공유된다는 점이다.
first_exam = Exam()
first_exam.writing_grade = 80
second_exam = Exam()
second_exam.writing_grade = 75
print(f'Second {second_exam.writing_grade} is right')
print(f'First {first_exam.writing_grade} is wrong')
>>>
Second 75 is right
First 75 is wrong
그것을 해결하기 위해서는, 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('Grade must be between 0 and 100')
self._values[instance] = value
이 방법은 간단하면서도, 잘 동작하지만, "메모리 누수"라는 문제점이 남아있다.
_values 딕셔너리는 프로그램의 수명 동안 __set__에 전달된 모든 Exam 인스턴스의 참조를 저장하고 있고,
때문에 인스턴스의 참조 개수가 절대로 0이 되지 않아 가비지 컬렉터가 정리하지 못하게 된다.
이럴땐, 파이썬의 내장 모듈 weakref를 사용하면 된다.
이 모듈은 _values에 사용한 간단한 딕셔너리를 대체할 수 있는 WeakKeyDictionary라는 특별한 클래스를 제공한다.
WeakKeyDictionary 클래스 고유의 동작은 런타임에 마지막으로 남은 Exam 인스턴스의 참조를 갖고 있다는 사실을 알면 키 집합에서 Exam 인스턴스를 제거하는 것이다.
class Grade:
def __init__(self):
self._values = WeakKeyDictionary()
# ...
'Programming Language > Python' 카테고리의 다른 글
Effective Python. 메타클래스로 서브클래스를 검증하자. (0) | 2021.03.03 |
---|---|
Effective Python. 지연 속성에는 __getattr__, __getattribute__, __setattr__을 사용하자. (0) | 2021.03.02 |
Effective Python. @property, @{property}.setter 사용 (0) | 2021.03.02 |
Effective Python. 메타클래스와 속성 (0) | 2021.03.02 |
[파이썬/Python] List 형태의 String을 List로. List 형태의 Dict를 Dict로. (1) | 2020.05.21 |