본문 바로가기

Computer/Python

Property와 descriptor (디스크립터)

반응형

지난 포스트에서 클래스의 어트리뷰트의 값을 정하고 불러오는 @property와 setter, getter 메소드에 대하여 알아봤습니다. 하지만 @property 데코레이터의 단점으로는 @property가 데코레이션하는 메서드를 같은 클래스에 속하는 여러 애트리뷰트로 사용할 수 없고 서로 무관한 클래스 사이에서 @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('!')

    @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를 지정하고 관련 메서드를 일일히 작성해야합니다. 이런 경우에는 파이썬의 디스크립터 (descriptor) 프로토콜을 이용할 수 있습니다. 디스크립터 프로토콜은 파이썬에서 애트리뷰터 접근을 해석하는 방법을 __get__, __set__ 메서드를 이용해 정의하고 이 두 메서드를 이용해 같은 점수 검증 동작을 여러 다른 애트리뷰트에 적용할 수 있습니다.


먼저 다음과 같이 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(f"!")
        self._value = value

그리고 Grade의 인스턴스인 클래스 애트리뷰트가 들어 있는 Exam 클래스를 정의하고 Exam 인스턴스에 있는 디스크립터 애트리뷰트에 접근하면,

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" 로 애트리뷰터로 읽으면 파이썬은 Exam.__dict__['writing_grade'].__get__(exam, Exam) 으로 해석합니다.
  • Exam 인스턴스에 writing_grade라는 이름의 애트리뷰트가 없으면 파이썬은 Exam 클래스의 애트리뷰트를 대신 사용하고 이 클래스의 애트리뷰터가 디스크립터 프로토콜로 (__set__, __get__)로 정의된 객체라면 디스크립터 프로토콜을 따라 애트리뷰트를 정의합니다.

다음과 같이 한 Exam 인스턴스에 정의된 여러 애트리뷰트에 접근할 경우에는 잘 동작하지만 여러 Exam 인스턴스 객체에 대해 애트리뷰트 접근을 시도하면 예기치 않게 동작합니다. 두 번쨰 Exam 인스턴스의 writing_grade 애트리뷰트 값을 변경하니 첫 번째 Exam 인스턴스의 writing_grade 애트리뷰트 값까지 변경되어버렸습니다.

>>> first_exam = Exam()
>>> first_exam.writing_grade = 82
>>> first_exam.science_grade = 99
>>> print(f"Writing {first_exam.writing_grade}, Science {first_exam.science_grade}")
Writing 82, Science 99

>>> second_exam = Exam()
>>> second_exam.writing_grade = 75
>>> print(f"Writing {second_exam.writing_grade}, Science {second_exam.science_grade}")
Writing 75, Science 99
>>> print(f"First Writing {first_exam.writing_grade}")
First Writing 75

문제는 writing_grade 클래스 애트리뷰트로 한 Grade 인스턴스를 모든 Exam 인스턴스가 공유한다는 점입니다. 프로그램이 실행되는 동안 Exam 클래스가 처음 정의될 때, 이 애트리뷰트에 대한 Grade 인스턴스가 단 한 번만 생성되고 Exam 인스턴스가 생성될 때마다 매번 Grade 인스턴스가 생성되지 않습니다.

이를 해결하기 위해서는 Grade 클래스가 각각의 유일한 Exam 인스턴스에 대해 따로 값을 추적하게 해야 하며 인스턴스 별 상태를 딕셔너리에 저장하면 구현이 가능합니다. 즉, Exam 클래스 애트리뷰트에 대해서 Grade 인스턴스가 한 번 생성되지만 Exam 인스턴스 별로 값을 별도로 저장하는 것이죠.

class Grade:
    def __init__(self):
        self._value = {}

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return self._value.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError(f"!")
        self._value[instance] = value

확인해보면 잘 동작합니다.

>>> first_exam = Exam()
>>> first_exam.writing_grade = 82
>>> first_exam.science_grade = 99
>>> second_exam = Exam()
>>> second_exam.writing_grade = 75
>>> print(f"First Writing {first_exam.writing_grade}, Second Writing {second_exam.writing_grade}")
First Writing 82, Second Writing 75

그렇다면 파이썬 descriptor는 왜 필요할까요? 예를 들어 어떤 클래스의 멤버 변수를 100이라고 설정하고 싶은데, 0 이상만 넣겠다는 제한을 걸어주고 싶습니다. Descriptor 없이 이를 구현하려면 맨 위의 예제처럼 property 호출 시마다 매 번 함수를 호출해주어야 합니다. 그렇다고 멤버 변수에 직접 접근해서 if/else 문을 직접적으로 구현하기에는 보기에도 안좋고 encapsulation이라는 대원칙에 어긋나는 안티패턴이라고 할 수 있겠죠. 

따라서 클래스 속성에 접근하되 이 속성에 값을 부여하거나 얻을 때 원하는 로직에 따라 함수처럼 동작하게 만들고 이를 암묵적으로 실행되게끔 encapsulate 시킨 것이 descriptor 입니다. 즉, 위 예제의 "_check_grade" 함수를 우리가 다룰 메인 클래스에서는 따로 구현시킬 필요가 없이 멤버 변수 접근만으로도 가능하게 한 것이죠.

반응형

'Computer > Python' 카테고리의 다른 글

파이썬 dataclasses 표준 라이브러리  (3) 2022.03.31
정규표현식을 이용해 문자열에서 숫자 찾기  (0) 2022.03.25
ModuleNotFoundError 와 ImportError  (0) 2021.08.26
Vectorization  (0) 2021.08.14
"is" vs "=="  (0) 2021.08.11