본문 바로가기
Programming Language/Python

Effective Python. 재사용 가능한 @property 메서드에는 디스크립터를 사용하자.

by 알파해커 2021. 3. 2.
반응형

"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()
    
    # ...

 

 

반응형

댓글