파이썬 코딩의 기술
Chapter4. 메타클래스와 속성
- 메타클래를 이용하면 파이썬의 class문을 가로채서 클래스가 정의될 때마다 특별한 동작을 제공할 수 있다
- 또 하나의 강력한 기능은 속성 접근을 동적으로 사용자화하는 파이썬의 내장 기능이다
- 동적 속성은 객체들을 오버라이드하다가 예상치 못한 부작용을 일으키게 할 수 있다
- 최소 놀랍 규칙을 따르자
29. getter와 setter method 대신에 일반 속성을 사용하자
- 간단한 공개 속성을 사용하여 새 클래스 인터페이스를 정의하고 setter와 getter method는 사용하지 말자
- 객체의 속성에 접근할 때 특별한 동작을 정의하려면 @property를 사용하자
- @property method에서 최소 놀람 규칙을 따르고 이상한 부작용은 피하자
- @property method가 빠르게 동작하도록 만들자. 느리거나 복잡한 작업은 일반 method로 하자
class OldResistor(object):
def __init__(self, ohms):
self._ohms = ohms
def get_ohms(self):
return self._ohms
def set_ohms(self, ohms):
self._ohms = ohms
r0 = OldResistor(50e3)
r0.set_ohms(10e3)
- 이런 getter와 setter를 사용하는 방법은 간단하지만 파이썬답지 않다
class Resistor(object):
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
r1.ohms += 53 # 즉석에서 증가시키기 같은 연산이 자연스럽고 명확해진다
class VoltageResistance (resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
@property
def voltage(self):
return self._voltage
@voltage.setter
def voltage(self, voltage)
self._voltage = voltage
self.current = self._voltage / self.ohms
r2 = VoltageResistance(1e3)
r2.voltage = 10
- setter와 getter method의 이름이 의도한 property 이름과 일치한다(voltage)
class BoundResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if ohms <= 0:
raise ValueError('%f ohms must be > 0' % ohms)
self._ohms = ohms
class FixedResistance(Resistor):
# ...
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if hasattr(self, '_ohms'):
raise AttributeError("Can't set attribute")
self._ohms = ohms
- property에 setter를 설정하면 클래스에 전달된 값들의 타입을 체크하고 값을 검증할 수도 있다
- 부모 클래스의 속성을 불변으로 만드는데도 @property를 사용할 수 있다
30. 속성을 리팩토링하는 대신 @property를 고려하자
- 기존의 인스턴스 속성에 새 기능을 부여하려면 @property를 사용하자
- @property를 사용하여 점점 나은 데이터 모델로 발전시키자
- @property를 너무 많이 사용한다면 클래스와 이를 호출하는 모든 곳을 리팩토링하는 방안을 고려하자
class Bucket(object):
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return ('Bucket(max_quota=%d, quota_consumed=%d)' %
(self.max_quota, self.quota_consumed))
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
# 새 기간의 할당량을 리셋함
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
# 새 기간의 할당량을 채움
assert self.quota_consumed == 0
self.max_quota = amount
else:
# 기간 동안 할당량을 소비함
assert self.max_quota >= self.quota_consumed
self.quota_consumed += delta
def fill(bucket, amount):
now = datatime.now()
if now - bucket.reset_time > bucket.period_delta:
bucket.quota = 0
bucket.reset_time = now
bucket.quota += amount
def deduct(bucket, amount):
now = datatime.now()
if now - bucket.reset_time > bucket.period_delta:
return False
if bucket.quota - amount < 0:
return False
bucket.quota -= amount
return True
31. 재사용 가능한 @property 메서드에는 디스크립터를 사용하자
- 직접 디스크립터 클래스를 정의하여 @property 메서드의 동작과 검증을 재사용하자
- WeakKeyDictionary를 사용하여 디스크립터 클래스가 메모리 누수를 일으키지 않게 하자
- getattribute가 디스크립터 프로토콜을 사용하여 속성을 얻어오고 설정하는 원리를 정확히 이해하려는 함정에 빠지지 말자
class Grade(object):
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None: return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._values[instance] = value
class Exam(object):
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
32. 지연 속성에는 getattr, getattribute, setattr을 사용하자
- 객체의 속성을 지연 방식으로 로드하고 저장하려면 getattr과 setattr을 사용하자
- getattr은 존재하지 않는 속성에 접근할 때 한 번만 호출되는 반면에 getattribute는 속성에 접근할 때마다 호출된다는 점을 이해하자
- getattribute와 setattr에서 인스턴스 속성에 직접 접근할 때 super()(즉, object 클래스)의 메서드를 사용하여 무한 재귀가 일어나지 않게 하자
class LazyDB(object):
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = 'Value for %s' % name
setattr(self, name, vlaue)
return value
data = LazyDB()
print('Before:', data.__dict__)
print('foo:', data.foo)
print('After:', data.__dict__)
class LoggingLazyDB(LazyDB):
def __getattr__(self, name):
print('Called __getattr__(%s)' % name)
return super().__getattr__(name)
data = LoggingLazyDB()
print('exists:', data.exists)
print('foo:', data.foo)
print('foo:', data.foo())
- getattr이 property loading을 한 번 실행하면 다음 접근부터는 기존 결과를 가져온다
class ValidatingDB(object):
def __init__(self):
self.exists = 5
def __getattribute__(self, name):
print('Called __getattribute__(%s)' % name)
try:
return super().__getattribute__(name)
except AttributeError:
value = 'Value for %s' % name
setattr(self, name, vlaue)
return value
data = ValidatingDB()
print('exists:', data.exists)
print('foo:', data.foo)
print('foo:', data.foo)
class SavingDB(object):
def __setattr__(self, name, value):
# 데이터 저장
# ...
super().__setattr__(name, value)
class LoggingSavingDB(SavingDB):
def __setattr__(self, name, value):
print('Called __setattr__(%s, %r) % (name, value)')
super().__setattr__(name, value)
data = LoggingSavingDB()
print('Before: ', data.__dict__)
data.foo = 5
print('After: ', data.__dict__)
data.foo = 7
print('Finally: ', data.__dict__)
class DictionaryDB(object):
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
data_dict = super().__getattribute__('_data')
return data_dict[name]
33. 메타클래스로 서브클래스를 검증하자
- 서브클래스 타입의 객체를 생성하기에 앞서 서브클래스가 정의 시점부터 제대로 구성되어음을 보장하려면 메타클래스를 사용하자
- 파이썬 2와 파이썬 3의 메타클래스 문법은 약간 다르다
- 메타클래스의 new 메서드는 class 문의 본문 전체가 처리된 후에 실행된다
class Meta(type):
def __new__(meta, name, bases, class_dict):
print((meta, name, bases, class_dict))
return type.__new__(meta, name, bases, class_dict)
class MyClass(object, metaclass=Meta):
stuff = 123
def foo(self):
pass
# 파이썬2
class Meta(type):
def __new__(meta, name, bases, class_dict):
# ...
class MyClassInPython2(object):
__metaclass__ = Meta
# ...
- 메타클래스는 클래스의 이름, 클래스가 상속하는 부모 클래스, class 본문에서 정의한 모든 클래스 속성에 접근할 수 있다
클래스가 정의도기 전에 모든 파라미터를 검증하려면 Meta.new메서드에 기능을 추가하면 된다
class ValidataPolygon(type):
def __new__(meta, name, bases, class_dict):
# 추상 Polygon 클래스는 검증하지 않음
if bases != (object, ):
if class_dict['sides'] < 3:
raise ValueError('Polygons need 3+ sides')
return type.__new__(meta, name, bases, class_dict)
class Polygon(object, metaclass=ValidationPolygon):
sizes = None # 서브클래스에서 설정함
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Triangle(Polygon):
sides = 3
class Line(Polygon):
sides = 1 # valueerror
34. 메타클래스로 클래스의 존재를 등록하자
- 클래스의 등록은 모듈 방식의 파이썬 프로그램을 만들 때 유용한 패턴이다
- 메타클래스를 이용하면 프로글매에서 기반 클래스로 서브클래스를 만들 때마다 자동으로 등록 코드를 실행할 수 있다
- 메타클래스를 이용해 클래스를 등록하면 등록 호출을 절대 빠뜨리지 않으므로 오류를 방지할 수 있다
class Serializable(object):
def __init__(self, *args):
self.args = args
def serializae(self):
return json.dumps({'args': self.args})
class Deserializable(Serializable):
@classmethod
def deserialize(cls, json_data):
params = json.loads(json_data)
return cls(*params['args'])
class BetterSerializable(object):
def __init__(self, *args):
self.args = args
def serializae(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args
})
registry = {}
def register_class(target_class):
registry[target_class.__name__] = target_class
def deserialize(data):
param = json.loads(data)
name = params['class']
target_class = registry[name]
return target_class(*params['args'])
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
35. 메타클래스로 클래스 속성에 주석을 달자
- 메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있다
- 디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룬다
- 메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 weakref 모듈을 모두 피할 수 있다
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.__name__(meta, name, bases, class_dict)
return cls
class DatabaseRow(object, metaclass=Meta):
pass
class Field(object):
def __init__(self, name):
self.name = name
self.internal_name = '_' + self.name
#...
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
foo = BetterCustomer()
print('Before:', repr(foo.first_name), foo__dict__)
foo.first_name = 'Euler'
print('After:', repr(foo.first_name), foo__dict__)
'프로그래머 > Python' 카테고리의 다른 글
[널널한 교수의 고급 파이썬] 01-1 파이썬 자료형과 참조 변수 (0) | 2020.12.11 |
---|---|
[Effective Python 복습] Chapter 5. 병행성과 병렬성 (0) | 2020.12.04 |
[Effective Python 복습] Chapter 3. 클래스와 상속 (0) | 2020.12.02 |
[파이썬] 파이썬 정리 for me (feat. FastCampus) (0) | 2020.05.30 |
Python - 문법 함수 | 생활코딩 강의 복습 | 프로그래밍 공부 (0) | 2018.12.30 |