ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 재사용 가능한 @property 메서드에는 디스크립터를 사용
    언어/파이썬 & 장고 2016. 10. 29. 17:42

    파이썬에 내장된 @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
        
    gal = Homework()
    gal.grade = 90


    학생들의 시험성적을 매기기 위해선 여러 과목으로 구성되어 있고 과목별로 점수가 있습니다.

    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


    시험영역마다 @property를 추가해야하기 때문에 코드가 장황해집니다. 이런 방법은 범용으로 사용하기에도 좋지 않습니다. 과제와 시험 이외의 항목에도 이 백분율 검증을 재사용하고 싶다면 @property와 checkgrade를 반복으로 작성해야 합니다. 


    파이썬에서는 이런 작업을 할 때 더 좋은 방법은 디스크립터를 사용하는 것입니다. 디스크립터 프로토콜(descriptor protocol)은 속성에 대한 접근을 언어에서 해석할 방법을 정의합니다. 디스크립터 클래스는 반복 코드 없이도 성적 검증 동작을 재사용할 수 있게 해주는 __get__과 __set__ 메서드를 제공할 수 있습니다. 이런 목적으로는 디스크립터가 믹스인보다도 좋은 방법입니다. 디스크립터를 사용하면 한 클래스의 서로 다른 많은 속성에 같은 로직을 재사용할 수 있기 때문입니다.


    이번에는 Grade 인스턴스를 클래스 속성으로 포함하는 새로운 Exam클래스를 정의합니다.  Grade 클래스는 디스크립터 프로토콜을 구현합니다. Grade클래스의 동작원리를 설명하기 전에 코드에서 Exam 인스턴스에 있는 이런 디스크립터 속성에 접근할 때 파이썬이 무슨일을 하는지 이해해야 합니다.


    class Grade:
    def __get__(*args, **kwargs):
    pass

    def __set__(*args, **kwargs):
    pass


    class Exam:
    # 클래스 속성
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()


    exam = Exam()
    exam.writing_grade = 40

    # 위 호출 코드는 다음과 같이 해석
    # Exam.__dict__['writing_grade'].__set__(exam,40)

    print(exam.writing_grade)

    # 위 호출 코드는 다음과 같이 해석
    # print(Exam.__dict__['writing_grade'].__get__(exam,Exam))


    이렇게 동작하게 만드는 건 object의 __getattribute__ 메서드입니다. 간단히 말하면 Exam 인스턴스에 writing_grade 속성이 없으면 파이썬은 대신 Exam 클래스의 속성을 사용합니다. 이 클래스의 속성이 __get__과 __set__ 메서드를 갖춘 객체라면 파이썬은 디스크립터 프로토콜을 따른다고 가정합니다.


    다음은 이런 동작과 Homework 클래스에서 @property를 성적 검증에 사용한 방법을 이해하고 Grade 디스크립터를 그럴듯하게 구현해본 첫 번째 시도입니다.

    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()


    first_exam = Exam()
    first_exam.writing_grade = 40
    first_exam.science_grade = 60

    print('writing', first_exam.writing_grade)
    print('science', first_exam.science_grade)

    # 결과
    # writing 40
    # science 60


    불행히도 위 코드는 잘못 구현되어 있어서 제대로 동작하지 않습니다. 한 Exam 인스턴스에 있는 여러 속성에 접근하는 것은 기대한 대로 동작합니다. 하지만 여러 Exam 인스턴스의 이런 속성에 접근하면 기대하지 않은 동작을 하게 됩니다.

    first_exam = Exam()
    first_exam.writing_grade = 40
    first_exam.science_grade = 60
    second_exam = Exam()
    second_exam.writing_grade = 70
    print('second', second_exam.writing_grade, 'is right')
    print('first', first_exam.writing_grade, 'is wrong')
    
    
    # 결과
    # second 70 is right
    # first 70 is wrong


    문제는 한 Grade 인스턴스가 모든 Exam 인스턴스의 writing_grade 클래스 속성으로 공유된다는 점입니다. 이 속성에 대응하는 Grade인스턴스는 프로그램에서 Exam 인스턴스를 생성할 때마다 생성되는게 아니라 Exam클래스를 처음 정의할 때 한 번만 생성됩니다.

    이 문제를 해결하려면 각 Exam 인스턴스 별로 값을 추적하는 Grade 클래스가 필요합니다. 여기서는 딕셔너리에 각 인스턴스의 사애를 저장하는 방법으로 값을 추적합니다.

    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


    이 구현은 간단하면서도 잘 동작하지만 여전히 문제점이 있습니다. 바로 메모리 누수입니다. _values 딕셔너리는 프로그램의 수명 동안 __set__에 전달된 모든 Exam 인스턴스의 참조를 저장합니다. 결국 인스턴스의 참조 개수가 절대로 0이 되지 않아 가비지 컬렉터가 정리하지 못하게 됩니다.

    파이썬의 내장 모듈 weakref를 사용하면 이 문제를 해결할 수 있습니다. 이 모듈은 _values에 사용한 간단한 딕셔너리를 대체할 수 있는 WeakKeyDictionary라는 특별한 클래스를 제공합니다. WeakKeyDictionary클래스 고유의 동작은 런타임에 마지막으로 남은 Exam 인스턴스의 ㅏㅁ조를 갖고 있다는 사실을 알면 키 집합에서 Exam 인스턴스를 제거하는 것입니다. 파이썬이 대신 참조를 관리해주고 모든 Exam인스턴스가 더는 사용되지 않으면 _values 딕셔너리가 비어 있게 합니다.


    이제 다음과 같은 Grade 디스크립터 구현을 사용하면 모두 기대한 대로 동작합니다.

    import weakref
    
    
    class Grade:
        def __init__(self):
            self._values = weakref.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 = 40
    first_exam.science_grade = 60
    second_exam = Exam()
    second_exam.writing_grade = 70
    print('second', second_exam.writing_grade, 'is right')
    print('first', first_exam.writing_grade, 'is right')
    
    
    # 결과
    # second 70 is right
    # first 40 is right

    요약

    직접 디스크립터 클래스를 정의하여 @property 메서드의 동작과 검증을 재사용

    WeakKeyDictionary를 사용하여 디스크립터 클래스가 메모리 누수를 일으키지 않게 해야 함

    __getattribute__가 디스크립터 프로토콜을 사용하여 속성을 얻어오고 설정하는 원리를 정확하게 이해하려는 함정에 빠지면 안됨


    댓글