게터와 세터 메서드
- 다른 언어에서 파이썬으로 넘어온 프로그래머들은 자연스레 클래스에 게터(
getter
)와 세터(setter
) 메서드를 명시적으로 구현하려고 함- 이런 게터와 세터를 사용하는 방법은 간단하지만 파이썬답지 않음
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)
print("Before: %5r" % r0.get_ohms())
r0.set_ohms(10e3)
print("After: %5r" % r0.get_ohms())
>>>
Before: 50000.0
After: 10000.0
- 게터와 세터 메서드는 특히 즉석에서 증가시키기 같은 연산에는 사용하기 어려움
r0.set_ohms(r0.get_ohms() + 5e3)
- 이런 유틸리티 메서드는 클래스의 인터페이스를 정의하는 데 도움이 됨
- 기능을 캡슐화하고 사용법을 검증하고 경계를 정의하기 쉽게 해줌
- 이런 요소는 클래스가 시간이 지나면서 발전하더라도 호출하는 쪽 코드를 절대 망가뜨리지 않도록 설계할 때 중요한 목표가 됨
property 메서드
- 파이썬에서는 명시적인 게터/세터를 구현할 일이 거의 없음
- 대신 항상 간단한 공개 속성부터 구현하기 시작해야 함
class Resistor(object):
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
r1.ohms += 5e3 # 즉석에서 증가시키기 같은 연산이 자연스럽고 명확
- 나중에 속성을 설정할 때 특별한 동작이 일어나야 하면
@property
데코레이터(decorator
)와 이에 대응하는setter
속성을 사용하는 방법으로 바꿀 수 있음- 여기서는
Resistor
의 새 서브클래스를 정의하여voltage
프로퍼티를 할당하면current
값이 바뀌게 해봄
- 여기서는
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
@property
def voltage(self):
return self._voltage
@voltage.setter # voltage에 맞게 current 프로퍼티를 업데이트하게끔 setter 메서드 설정
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms
r2 = VoltageResistance(1e3)
print("Before: %5r amps" % r2.current)
r2.voltage = 10 # voltage에 할당 => setter 메서드 실행
print("After: %5r amps" % r2.current)
>>>
Before: 0 amps
After: 0.01 amps
- 다음은 모든 저항값이 0옴보다 큼을 보장하는 클래스
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 ohms must be > 0" % ohms)
self._ohms = ohms
- 속성에 올바르지 않은 저항값을 할당하면 예외 발생
r3 = BoundedResistance(1e3)
r3.ohms = 0
>>>
ValueError: 0.000000 ohms must be > 0
- 생성자에 올바르지 않은 값을 넘겨도 예외 발생
BoundedResistance.__init__
가self.ohms = -5
를 할당하는Resistor.__init__
를 호출하기 때문- 여기에서
BoundedResistance
의@ohms.setter
메서드가 호출되어 검증 코드가 실행됨
- 여기에서
BoundedResistance(-5)
>>>
ValueError: -5.000000 ohms must be > 0
- 부모 클래스의 속성을 불변(
immutable
)으로 만드는 데도@property
를 사용할 수 있음
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
- 이 객체를 생성하고 나서 프로퍼티에 할당하려고 하면 예외가 발생
r4 = FixedResistance(1e3)
r4.ohms = 2e3
>>>
AttributeError: Can't set attribute
@propery
메서드의 단점- 속성에 대응하는 메서드를 서브클래스에서만 공유할 수 있다는 점:
- 서로 관련이 없는 클래스는 같은 구현을 공유하지 못함
- 하지만 파이썬은 재사용 가능한 프로퍼티 로직을 비롯해 다른 많은 쓰임새를 가능하게 하는 디스크립터(
descriptor
)도 지원
- 속성에 대응하는 메서드를 서브클래스에서만 공유할 수 있다는 점:
- 마지막으로
@property
메서드로 세터와 게터를 구현할 떄 예상과 다르게 동작하지 않게 해야 함- 예를 들면 게터 프로퍼티 메서드에 다른 속성을 설정하지 말아야 함
class MysteriousResistor(Resistor):
@property
def ohms(self):
self.voltage = self._ohms * self.current # self._ohms를 제외한 다른 속성을 설정 => 이상 동작 발생
return self._ohms
#...
r7 = MysteriousResistor(10)
r7.current = 0.01
print("Before: %5r" % r7.voltage)
r7.ohms
print("After: %5r" % r7.voltage)
>>>
Before: 0
After: 0.1
- 최선의 정책
@property.setter
메서드에서만 관련 객체의 상태를 수정하는 것- 호출하는 쪽이 객체에서 일어날 것이라고 예측하지 못할 만한 다른 부작용은 모두 피해야 함: 최소 놀람 규칙(
rule of least surprise
)
- 사용자는 다른 파이썬 객체가 그렇듯이 클래스의 속성이 빠르고 쉬울 것이라고 기대할 것임
- 복잡하고 느린 작업은 일반 메서드로 하는 것이 좋음