-
[Python] Tip - 메타클래스로 클래스 속성에 주석달기언어/파이썬 & 장고 2016. 12. 6. 21:00
메타클래스로 구현할 수 있는 기능 중 하나는 클래스를 정의한 이후에, 하지만 그 클래스를 실제로 사용하기 전에 프로퍼티를 수정하거나 주석을 붙이는 것입니다. 보통은 이 기법을 디스크립터와 함께 사용하여 클래스에서 디스크립터를 어떻게 사용하는지 자세히 조사한 정보를 디스크립터에 제공합니다.
예를 들어 고객 데이터베이스의 로우를 표현하는 새 클래스를 정의한다고 가정합니다. 데이터베이스 테이블의 각 컬럼에 대응하는 클래스의 프로퍼티가 있어야 합니다. 따라서 프로퍼티를 컬럼 이름과 연결하는 데 사용할 디스크립터 클래스를 다음과 같이 정의합니다.
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)Field 디스크립터에 저장할 컬럼 이름이 있으면 내장함수 setattr과 getattr을 사용해서 모든 인스턴스별 상태를 인스턴스 딕셔너리에 보호 필드로 직접 저장할 수 있습니다. 처음에는 이 방법이 메모리 누수를 피하려고 weakref로 디스크립터를 만드는 방법보다 훨씬 편리해 보입니다.
로우를 표현하는 클래스를 정의할 때는 각 클래스 속성에 대응하는 컬럼의 이름을 지정해야 합니다.
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') sufix = Field('sufix')
# Field 디스크립터가 인스턴스 딕셔너리 __dict__를 기대한 대로 수정한 방식을 볼 수 있음 foo = Customer() print('Before: ', repr(foo.first_name), foo.__dict__) foo.first_name = 'Euclid' print('After: ', repr(foo.first_name), foo.__dict__) # 결과 # Before: '' {} # After: 'Euclid' {'_first_name': 'Euclid'}
위 결과는 중복되는 것처럼 보입니다. 이미 class문 본문에서 Field 객체를 생성하여 Customer.first_name에 할당할 때 필드의 이름을 선언했습니다. 왜 필드 이름(여기서는 first_name)을 Field 생성자에도 넘겨야 될까요?
문제는 Customer 클래스 정의에서 연산 순서가 왼쪽에서 오른쪽으로 읽는 방식과는 반대라는 점입니다. 먼저 Field 생성자는 Field('first_name') 형태로 호출합니다. 다음을 이 호출의 반환 값을 Customer.field_name에 할당합니다. 그러므로 Field에서는 자신이 어떤 클래스 속성에 할당될지 미리 알 방법이 없습니다.
중복성을 제거하려면 메타클래스를 사용하면 됩니다. 메타클래스를 이용하면 class 문을 직접 후킹하여 class 본문이 끝나자마자 원하는 동작을 처리할 수 있습니다. 이 예제에서는 필드 이름을 수동으로 여러 번 지정하지 않고 메타클래스를 사용하여 Field.name과 Field.internal_name을 디스크립터에 자동으로 할당합니다.
class Field:
"""
메타클래스를 사용해도 필드 디스크립터는 변경이 거의 없음. 유일한 차이는 생성자에 인수를 넘길 필요가 없음.
대신 필드 디스크립터의 속성은 위의 Meta.__new__메서드로 설정
"""
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 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 BetterCustomer(DatabaseRow):
"""
메타클래스, 새 DatabaseRow 기반 클래스, 새 Field 디스크립터를 사용하면 데이터베이스 로우를 표현하는 클래스의 정의에 이전 같은 중복이 생기지 않음
"""
# 클래스 속성
first_name = Field()
last_name = Field()
prefix = Field()
sufix = Field()
foo = BetterCustomer()
print('Before: ', repr(foo.first_name), foo.__dict__)
foo.first_name = 'Euclid'
print('After: ', repr(foo.first_name), foo.__dict__)
# 결과
# Before: '' {}
# After: 'Euclid' {'_first_name': 'Euclid'}요약
메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있음
디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룸
메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 weakref 모듈을 모두 피할 수 있음