[Effective Python] BW 50. __set_name__으로 클래스 애트리뷰트를 표시하라

전민수·2023년 8월 4일
0

EffectivePython

목록 보기
10/10

메타 클래스의 유용한 기능 중 하나로
클래스가 정의된 후 실제로 사용되기 이전 시점에 프로퍼티를 변경하거나 표시할 수 있습니다.

예를 들어 고객 데이터베이스의 row를 표현하는 새 클래스를 정의해봅시다.
테이블의 각 column에 해당하는 프로퍼티를 클래스에 정의하고 싶습니다.
다음 코드는 애트리뷰트와 column 이름을 연결하는 디스크럽터 클래스입니다.

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)

여기서 name에는 column 이름이 들어갑니다.

class Customer:
    # 클래스 애트리뷰트
    first_name = Field('first_name')
    last_name = Field('last_name')
    prefix = Field('prefix')
    suffix = Field('suffix')

이렇게 되면 Field 디스크럽터를 이용하여 Customer의 클래스 애트리뷰트와 column 이름을 연결할 수 있습니다.

cust = Customer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '유클리드'
print(f'이후: {cust.first_name!r} {cust.__dict__}')

# 이전 : '' {}
# 이후 : '유클리드' {'_first_name': '유클리드'}

Field 디스크럽터가 __dict__ 인스턴스 딕셔너리를 변화시키는 것을 확인할 수 있습니다.

하지만

이 클래스 정의는 중복이 많습니다. 즉 좌변과 우변에 중복되는 데이터가 들어간다는 것입니다.

파이썬에서는 first_name = Field('first_name')이라는 구문이
Field('first_name')을 통해 Field 생성자를 호출하고,
반환된 값을 first_name =에 처리하여 Customer.field_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()

결과를 확인해보면, 기존의 코드와 같이 동작합니다.

cust = Customer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '유클리드'
print(f'이후: {cust.first_name!r} {cust.__dict__}')

# 이전 : '' {}
# 이후 : '유클리드' {'_first_name': '유클리드'}

코드가 중복없이 깔끔하죠. 마음이 편안해집니다.

하지만

만약 DatabaseRow를 상속하는 것을 잊어버리거나 클래스 계층 구조로 인한 제약 때문에 DatabaseRow를 상속할 수 없는 경우!
정의하는 클래스가 Field 클래스를 프로퍼티에 사용할 수 없다는 것입니다.
DatabaseRow를 상속하지 않으면 코드가 깨집니다.

set_name

이 문제를 해결하는 방법은 __set_name__ 특별 메서드를 사용하는 것입니다.

  • 클래스가 정의될 때마다 파이썬은 해당 클래스 안에 들어 있는 디스크립터 인스터스의 __set_name__을 호출합니다.
  • __set_name__은 디스크립터 인스턴스를 소유 중인 클래스와 디스크립터 인스턴스가 대입될 애트리뷰트 이름을 인자로 받습니다.

다음 코드는 메타클래스를 정의하는 방식이 아닌, Meta.__new__가 하던 작업을 __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)


class FixedCustomer:
    first_name = Field()
    last_name = Field()
    prefix = Field()
    suffix = Field()

cust = FixedCustomer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '메르센'
print(f'이후: {cust.first_name!r} {cust.__dict__}')

# 이전 : '' {}
# 이후 : '메르센' {'_first_name': '메르센'}
profile
Learning Mate

0개의 댓글