메타클래스를 사용하면 파이썬의 class 문을 가로채서 클래스가 정의될 떄마다 특별한 동작을 제공할 수 있다.
다른 언어를 사용하다 파이썬을 접한 프로그래머들은 게터와 세터를 명시적으로 정의한다.
class OldResistor:
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)
print('이전:', r0.get_ohms())
r0.set_ohms(10e3)
print('이후:', r0.get_ohms())
이전: 50000.0 이후: 10000.0
특히 필드 값을 증가시키는 연산 등의 경우에는 이런 메서드를 사용하면 코드가 지저분해 진다.
r0.set_ohms(r0.get_ohms() - 4e3)
assert r0.get_ohms() == 6e3
파이썬에서는 단순한 공개 애트리뷰트로부터 구현을 시작하라
class Resistor:
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
애트리뷰트를 사용하면 제자리에서 증가시키는 등이 연산이 더 자연스럽고 명확해진다.
r1.ohms += 5e3
나중에 애트리뷰트가 설정될 떄 특별한 기능을 수행해야 한다면, 애트리뷰트를 @property 데코레이터와 대응하는 setter 애트리뷰트로 옮겨갈 수 있다.
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
이제 voltage 프로퍼티에 대입하면 voltage 세터 메서드가 호출되고, 이 메서드는 객체의 current 애트리뷰트를 변경된 전압 값에 맞춰 갱신한다.
r2 = VoltageResistance(1e3)
print(f'이전: {r2.current:.2f} 암페어')
r2.voltage = 10
print(f'이후: {r2.current:.2f} 암페어')
이전: 0.00 암페어 이후: 0.01 암페어
프로퍼티에 대해 setter를 지정하면 타입을 검사하거나 클래스 프로퍼티에 전달된 값에 대한 검증을 수행할 수 있다.
class BoundedResistance(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'저항 > 0이어야 합니다. 실제 값: {ohms}')
self._ohms = ohms
잘못된 저항 값을 대입하면 예외가 발생한다.
r3 = BoundedResistance(1e3)
r3.ohms = 0
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-13-028e916e3f75> in <module> ----> 1 r3.ohms = 0 <ipython-input-10-71ca84303584> in ohms(self, ohms) 10 def ohms(self, ohms): 11 if ohms <= 0: ---> 12 raise ValueError(f'저항 > 0이어야 합니다. 실제 값: {ohms}') 13 self._ohms = ohms ValueError: 저항 > 0이어야 합니다. 실제 값: 0
생성자에 잘못된 값을 넘기는 경우에도 예외가 발생한다.
BoundedResistance(-5)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-14-9fc5cf3426e1> in <module> ----> 1 BoundedResistance(-5) <ipython-input-10-71ca84303584> in __init__(self, ohms) 1 class BoundedResistance(Resistor): 2 def __init__(self, ohms): ----> 3 super().__init__(ohms) 4 5 @property <ipython-input-5-299937d78e30> in __init__(self, ohms) 1 class Resistor: 2 def __init__(self, ohms): ----> 3 self.ohms = ohms 4 self.voltage = 0 5 self.current = 0 <ipython-input-10-71ca84303584> in ohms(self, ohms) 10 def ohms(self, ohms): 11 if ohms <= 0: ---> 12 raise ValueError(f'저항 > 0이어야 합니다. 실제 값: {ohms}') 13 self._ohms = ohms ValueError: 저항 > 0이어야 합니다. 실제 값: -5
심지어 @property를 사용해 부모 클래스에 정의된 애트리뷰트를 불변으로 만들 수도 있다.
class FixedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if hasattr(self, '_ohms'):
raise AttributeError("Ohms는 불변객체입니다")
self._ohms = ohms
r4 = FixedResistance(1e3)
r4.ohms = 2e3
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-17-9addc5737525> in <module> ----> 1 r4.ohms = 2e3 <ipython-input-15-c536541bb2ce> in ohms(self, ohms) 10 def ohms(self, ohms): 11 if hasattr(self, '_ohms'): ---> 12 raise AttributeError("Ohms는 불변객체입니다") 13 self._ohms = ohms AttributeError: Ohms는 불변객체입니다
@property 메서드를 사용해 세터와 게터를 구현할 떄는 게터나 세터 구현이 예기치 않은 동작을 수행하지 않도록 만들어야 한다.
예를 들어 게터 프로퍼티 메서드 안에서 다른 애트리뷰트를 설정하면 안된다.
class MysteriousResistor(Resistor):
@property
def ohms(self):
self.voltage = self._ohms * self.current
return self._ohms
@ohms.setter
def ohms(self, ohms):
self._ohms = ohms
r7 = MysteriousResistor(10)
r7.current = 0.01
print(f'이전: {r7.voltage:.2f}')
r7.ohms
print(f'이후: {r7.voltage:.2f}')
이전: 0.00 이후: 0.10
게터나 세터를 정의할 때 가장 좋은 정책은 관려이 있는 객체 상태를 @property.setter 메서드 안에서만 변경하는 것이다.
@property의 가장 큰 단점은 애트리뷰트를 처리하는 메서드가 하위 클래스 사이에서만 공유될 수 있다는 것이다.