파이썬코딩의 기술 - BW50 : __set_name__으로 클래스 애트리뷰트를 표시하라
클래스가 정의된 후 클래스가 실제로 사용되기 이전인 시점에 프로퍼티를 변경하거나 표시할 수 있는 기능
- (활용) 애트리뷰트가 포함된 클래스 내부에서 애트리뷰트 사용을 좀 더 자세히 관찰하고자 디스크립터를 쓸 때 이런 접근 방식을 활용함
- (예) 고객 데이터베이스의 로우(row)를 표현하는 새 클래스를 정의한다고 하자.
1) 데이터베이스 테이블의 각 컬럼에 해당하는 프로퍼티를 클래스에 정의
2) 애트리뷰트와 컬럼 이름을 연결하는 디스크립터 클래스
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)
- setattr 내장 함수 : 인스턴스별 상태를 직접 인스턴스 딕셔너리에 저장할 수 있음
- getattr로 인스턴스의 상태를 읽을 수 있음
로우를 표현하는 클래스를 정의하려면, 애트리뷰트별로 해당 테이블 컬럼 이름을 지정하면 됩니다
class Customer:
# 클래스 애트리뷰트
first_name = Field('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')
위 클래스를 사용하기는 쉬움
- Field 디스크립터가 __dict__인스턴스 딕셔너리를 변화시킨다는 사실을 확인할 수 있음
cust = Customer()
print(f'이전: {cust.first_name!r} {cust._ _dict__}')
cust.first_name = '유클리드'
print(f'이후: {cust.first_name!r} {cust._ _dict__}')
>>>
이전: '' {}
이후: '유클리드' {'_first_name': '유클리드'}
- (문제) 중복이 많음
-> 클래스 안에서 왼쪽에 필드 이름을 이미 정의 (field_name = )
-> 굳이 같은 정보가 들어있는 문자열을 Field 디스크립터에게 다시 전달 (Field('first_name')) 해야 할 이유가 없음
class Customer:
# =의 좌변과 우변의 정보가 불필요하게 중복된다
first_name = Field('first_name')
...
- (해결) 중복을 줄이기 위해 메타클래스를 사용할 수 있음
- 메타클래스를 사용해 디스크립터의 Field.name과 Field.interanl_name을 자동으로 대입할 수 있음
( 메타클래스를 사용하면 class문에 직접 훅을 걸어서 class 본문이 끝나자마자 필요한 동작을 수행할 수 있음 )
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
- 메타클래스 사용 -> 생성자 인자가 없다는 점이 달라짐
- 생성자가 컬럼 이름을 받고, Meta.__new__ 메서드가 애트리뷰트를 설정
class Field:
def __init__(self):
# 이 두 정보를 메타클래스가 채워준다
self.name = None
self.internal_name = None
def __get__(self, instance, instance_type):
if instance is None:
ruturn self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
- (사용 결과) 메타클래스, 새로운 DatabaseRow 기반 클래스, Field 디스크립터
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를 상속하지 않으면 코드가 깨짐
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
cust = BrokenCustomer()
cust.first_name = '메르센'
>>>
Traceback ...
TypeError: attribute name must be string, not 'NoneType'
- (문제 해결방법) 디스크립터에 __set_name__ 특별 메서드(파이썬 3.6부터 도입)를 사용하는 것 입니다.
- 클래스가 정의될 때마다 파이썬은 해당 클래스 안에 들어있는 디스크립터 인스턴스의 __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)
- 특정 기반 클래스를 상속하거나 메타클래스를 사용하지 않아도 Field 디스크립터가 제공하는 기능을 모두 활용 가능
class BetterCustomer(DatabaseRow):
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': '메르센'}
### 기억해야할 내용 ###
- 메타클래스를 사용하면 어떤 클래스가 완전히 정의되기 전에 클래스의 애트리뷰트를 변경할 수 있음
- 디스크립터와 메타클래스를 조합하면 강력한 실행 시점 코드 검사와 선언적인 동작을 만들 수 있음
- __set_name__ 특별 메서드를 디스크립터 클래스에 정의하면 디스크립터가 포함된 클래스의 프로퍼티 이름을 처리할 수 있음
- 디스크립터가 변경한 클래스의 인스턴스 딕셔너리에 데이터를 저장하게 만들면 메모리 누수를 피할 수 있고, weakref 내장 메서드를 사용하지 않아도 됨