프로그래밍 언어/Python

디스크립터 __set_name__ 의 활용방법

JMDev 2023. 9. 14. 19:29

__ set_name __ 를 활용하기 이전에, 해당 디스크립터가 필요하게 된 상황을 알아보자

class Field:
    def __init__(self, name):
        self.name = name
        self.internal_name = '_' + self.name

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)
class Customer:
    first_name = Field('first_name')
    last_name = Field('last_name')
    prefix = Field('prefix')
    suffix = Field('suffix')

위 코드로 데이터를 표현해주는 객체(Customer)의 column를 Field 라는 프로퍼티 클래스를 만들어주었다.

하지만 이 방법에는 first_name 이라는 변수로 네임을 표현해줬는데

Field에 다시 네임을 다시 한번 기재해줘야 하는 중복이 발생하게 되었다. ex: Field('first_name')

이 문제를 해결하기 위해서 first_name이라는 변수를 어떻게든 활용하면 될거라고 생각할 수 있지만,

파이썬에서는 오른쪽에서 왼쪽으로 실행하는 구조를 갖고 있어

Field()가 먼저 실행 후 변수에 값이 할당된다. 이를 해결하기 위해서 메타클래스를 사용해볼 수 있는데

class Meta(type):
    def __new__(meta, name, bases, class_dict):
        for key, value in class_dict.items():
            if isinstance(value, Field):
                value.name = key
                value.internal_name = '_' + key
        cls = type.__new__(meta, name, bases, class_dict)
        return cls

class DatabaseRow(metaclass=Meta):
    pass
class Field:
    def __init__(self):
				# 이 부분을 메타클래스에서 채워주게 됩니다!
        self.name = None
        self.internal_name = None

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)

class BetterCustomer(DatabaseRow):
    first_name = Field()
    last_name = Field()
    prefix = Field()
    suffix = Field()

위 코드로 리팩토링하면서 생성자에 컬럼이름을 받지 않게 되었고,

메타클래스에서 애트리뷰트를 설정함으로써 중복을 없앨 수 있게 되었다

하지만 위 코드의 문제점은 DatabaseRow를 무조건 상속받아야 함으로부터 제약이 생기는데,

이 부분을 디스크럽터의 __ set_name __ 호출함으로써

메타클래스에서 애트리뷰트를 통제하는 로직을 리팩토링할 수 있게 된다.

class Field:
    def __init__(self):
        self.name = None
        self.internal_name = None
		# 디스크립터 인스턴스를 소유 중인 클래스와 대입될 에트리뷰트 이름을 인자로 받는다
    def __set_name__(self, owner, name):
        # 클래스 생성 시 모든 스크립터에 대해 이 메서드 호출
        self.name = name
        self.internal_name = '_' + name

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)

위 과정들을 통해 메타클래스가 어떤 클래스가 정의되기 전에 클래스의 애트리뷰트를 변경할 수 있고,

__ set_name __ 메서드로 디스크립터 클래스에 정의하면

디스크립터가 포함한 프로퍼티 이름을 처리할 수 있음을 알 수 있다.

디스크립터가 변경한 클래스 인스턴스 딕셔너리에 데이터 저장하게 만들면 누수를 피할 수 있고,

weakref 내장매서드를 쓰지 않아도 된다