들어가기 전
- 이 글은 이펙티브 파이썬을 공부하며 작성한 글입니다.
- 코드 : https://github.com/chickenchickenlove/effective-python/tree/master/item44
요약
- 새로운 클래스 인터페이스를 정의할 때, 간단히 public attribute도 시작하고 Setter / Getter를 가급적이면 사용하지 마라.
- 객체에 있는 Attribute에 접근할 때, 부가 기능이 필요한 경우 @property로 이를 구현할 수 있음.
- @property를 사용할 때, 최소 놀람의 법칙을 따르고 이상한 부작용을 만들지 마라.
- @property 메서드가 빠르게 실행되도록 유지. 느리거나 복잡한 작업의 경우, 일반적인 메서드를 사용하라.
- @property로 setter를 정의하면, self.host = host같은 메서드가 호출될 때 @property.setter 메서드가 호출되게 된다.
- super().__init__()를 호출해야 부모 클래스의 인스턴스가 초기화 된다. 이 때, 부모 클래스의 attribute를 자식 클래스에서 참조할 수 있게 됨.
- super().__init__()에서도 자식 클래스에 정의된 public attribute로 접근하게 되면, @property 메서드가 호출됨.
- @property를 이용하면 파이썬스럽게 Getter / Setter를 사용할 수 있으며 부가기능을 추가할 수 있음.
Item44. Setter / Getter 대신 Attribute를 사용하라.
# 단순 Setter / Getter를 사용하는 경우, 복잡한 작업할 때 지저분 해보임.
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(f'이전, {r0.get_ohms()}')
r0.set_ohms(10e3)
print(f'이후, {r0.get_ohms()}')
# 이 부분에 굉장히 잡음이 많음.
r0.set_ohms(r0.get_ohms() - 4e3)
assert r0.get_ohms() == 6e3
- 처음 파이썬을 접하는 사람들은 Getter / Setter 메서드를 위와 같이 직접 구현해서 사용함.
- 이런 Getter / Setter는 코드를 읽을 때 잡음을 생성함.
- 특히 값 증가/감소 연산 시, 많은 잡음을 생성함.
Getter / Setter는 내부 로직의 캡슐화, Validation 등의 부가 기능을 메서드에 포함해 캡슐화 할 수 있기 때문에 사용하면 좋은 기능이다. 파이썬에서 다른 언어처럼 Getter / Setter의 이점을 살리면서 사용할 수 있는 방법이 있을까?
1. 공개된 Attribute부터 코드를 작성해라.
class Resistor:
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
# Public Attribute로 사용하면, 연산할 때 좀 더 읽기 쉬워짐.
r1.ohms += 5e3
class VoltageResistor(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 = VoltageResistor(1e3)
print(f'이전: {r2.current:.2f} 암페어')
r2.voltage = 10
print(f'이후: {r2.current:.2f} 암페어')
Resistor 클래스부터 보자. 먼저 public Attribute로 ohms를 사용한다. 이렇게 작성된 코드에서 ohms에 값 증가/감소 연산을 할 때, 코드를 읽는데 어떤 불협화음도 존재하지 않는 것을 알 수 있다. 그런데 이렇게 작성해두면 다음 두 가지 문제점이 생각이 날 것이다.
- 값을 설정하거나 읽어올 때 Validation 같은 부가 기능을 추가하기 적합하지 않음.
- 내부 필드를 아는 것은 외부에서 내부 구현을 아는 것과 동일함. 확장 할 때 어려울 것임.
이런 걱정은 파이썬에서 제공하는 @property라는 데코레이터를 사용하면 모두 해결된다.
VoltageResistor를 보자. 여기서 주목할 부분은 Attribute를 self._voltage(Protected)로 선언하고, @property + @voltage.setter라는 데코레이터를 추가한 것이다. 이렇게 선언하면 public attribute에 접근하는 것처럼 r2.voltage로 접근할 수 있게 되며 부가 기능도 추가할 수 있게 된다. 만약 ohms의 확장이 필요하다면 self._ohms로 바꾼 후 ohms() 함수를 구현하고 @property 데코레이터를 붙여주기만 하면 된다.
2. @property를 이용해 Validation 부가기능 추가.
class Resistor:
def __init__(self, ohms):
# 공개 Attribute로 시작했다가, 이 녀석이 Setter를 호출하는 형태가 되어버림. (확장에 유리함)
# 그러나 잘 모른다면, 이해는 어려울 듯?
self.ohms = ohms
self.voltage = 0
self.current = 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'저항 > 0 이어야 합니다. 실제 값: {ohms}')
self._ohms = ohms
# super().__init__(ohms) -> self.ohms 여기서, @property가 호출됨. -> @ohms.setter가 호출됨.
r3 = BoundedResistance(1e3)
print(r3.ohms)
r3.ohms = 0
'r3.ohms = 10'이라는 구문이 실행되면, @ohms.setter 데코레이터가 붙은 함수가 실행된다. 이걸 이용해서 @ohms.setter 데코레이터가 붙은 메서드에 Validation 구문을 추가하면 된다.
여기서 주의해서 보면 좋을 부분은 다음과 같다.
- BoundedResistance(1e3)이 실행.
- super().__init__(ohms)가 실행.
- self.ohms = ohms가 실행. → 이것은 @ohms.setter 함수를 실행
- super().__init__(ohms) 완료
- BoundedResistance(1e3) 실행 완료.
이런 순서로 실행된다. 빨갛게 표시한 self.ohms = ohms에서 @ohms.setter 함수가 실행된다는 것에 주목해야한다.
3. @property를 이용해 불변 Attribute 만들기
# 부모 클래스의 Attribute를 불변으로 만들기.
class Resistor:
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
class FixedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
# self = FixedResistance임
# FixedResistance는 self.ohms를 Attribute로 가짐. (부모가 init 하니까)
# 부모가 init 하는 시점에는 self.ohms가 초기화 되어 있지 않은 상태에서 이 메서드가 호출되기 때문에 self._ohms = ohms로 처리함.
# 그 이후에는 self.ohms가 초기화 되었기 때문에 항상 에러를 발생시킴.
if hasattr(self, 'ohms'):
raise AttributeError(f'Ohms는 불변 객체입니다.')
self._ohms = ohms
r = FixedResistance(1e3)
r.ohms = 0
@property를 이용하면 부모 클래스에 정의된 public attribute를 불변으로 만들 수도 있다. 구현은 위와 같이 하면 되는데, 코드를 해석해보면 다음과 같다.
- super().__init__(ohms)가 호출
- self.ohms = ohms가 호출 → @ohms.setter 함수가 호출.
- @ohms.setter 함수에서 self는 FixedResistance임. 이 때, self에는 ohms라는 Attribute가 없음. self.ohms = ohms에 의해서 호출되었으므로, 아직까지 self.ohms가 초기화 되지 않은 상태이기 때문임. 따라서 hasattr()은 False로 처리되고, self._ohms = ohms가 호출됨.
- 이후 r.ohms = 0이 호출될 때, self는 self.ohms를 가지고 있기 때문에 hasattr()이 True로 처리되어 AttributeError가 Raise 됨.
4. @property에서 Getter 사용 시 주의할 점.
# Getter를 잘못 쓴 경우
class Resistor:
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
class MysteriousResistor(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
# Getter에서 다른 프로퍼티를 수정하면서 이상하게 됨.
self.voltage = self._ohms * self.current
return self._ohms
@ohms.setter
def ohms(self, ohms):
if hasattr(self, 'ohms'):
raise AttributeError(f'Ohms는 불변 객체입니다.')
self._ohms = ohms
# super().__init__(ohms) -> self.ohms 여기서, @property가 호출됨. -> @ohms.setter가 호출됨.
r = MysteriousResistor(1e3)
r.ohms = 0
@property가 붙은 ohms() 메서드가 self.ohms의 Getter 메서드 역할을 한다. Getter 메서드에서 내부적으로 사용하고 있는 필드를 수정하게 되면, 알 수 없는 인스턴스가 될 수 있다.
예를 들어 위 코드에서 print(r.ohms)를 할 때 마다 Voltage 값이 자동으로 계산되게 된다. 일반적으로 공개 API를 사용하는 사람들은 내부 구현을 모르는 것이 맞기 때문에 자동으로 계산되는 Voltage는 이 API를 사용하는 사용자들을 혼란스럽게 만든다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
Effective Python Item 28. 컴프리헨션 내부에는 수식을 2개까지만 써라 (0) | 2024.01.13 |
---|---|
Effective Python Item 45. 어트리뷰트를 리팩토링하는 대신 @property를 사용하라 (0) | 2024.01.13 |
Effective Python Item 36. Iterator / Generator를 다룰 때는 itertools를 사용하라 (0) | 2024.01.13 |
이펙티브 파이썬 Item 27. map과 filter 대신 컴프리헨션을 사용하라 (0) | 2023.10.21 |
이펙티브 파이썬 Item 26. functools.wrap을 사용해 함수 데코레이터를 정의하라 (0) | 2023.10.19 |