메타 클래스의 유용한 기능 중 하나로
클래스가 정의된 후 실제로 사용되기 이전 시점에 프로퍼티를 변경하거나 표시할 수 있습니다.
예를 들어 고객 데이터베이스의 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__
은 디스크립터 인스턴스를 소유 중인 클래스와 디스크립터 인스턴스가 대입될 애트리뷰트 이름을 인자로 받습니다.다음 코드는 메타클래스를 정의하는 방식이 아닌, 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': '메르센'}