#!/usr/bin/env python # coding: utf-8 # # 6. 메타클래스와 애트리뷰트 # # 메타클래스를 사용하면 파이썬의 class 문을 가로채서 클래스가 정의될 떄마다 특별한 동작을 제공할 수 있다. # ## 44. 세터와 게터 메서드 대신 평범한 애트리뷰트를 사용하라 # 다른 언어를 사용하다 파이썬을 접한 프로그래머들은 게터와 세터를 명시적으로 정의한다. # In[2]: class OldResistor: def __init__(self, ohms): self._ohms = ohms def get_ohms(self): return self._ohms def set_ohms(self, ohms): self._ohms = ohms # 하지만 파이썬 답지 않다. # In[3]: r0 = OldResistor(50e3) print('이전:', r0.get_ohms()) r0.set_ohms(10e3) print('이후:', r0.get_ohms()) # 특히 필드 값을 증가시키는 연산 등의 경우에는 이런 메서드를 사용하면 코드가 지저분해 진다. # In[4]: r0.set_ohms(r0.get_ohms() - 4e3) assert r0.get_ohms() == 6e3 # 파이썬에서는 단순한 공개 애트리뷰트로부터 구현을 시작하라 # In[5]: class Resistor: def __init__(self, ohms): self.ohms = ohms self.voltage = 0 self.current = 0 # In[6]: r1 = Resistor(50e3) r1.ohms = 10e3 # 애트리뷰트를 사용하면 제자리에서 증가시키는 등이 연산이 더 자연스럽고 명확해진다. # In[7]: r1.ohms += 5e3 # 나중에 애트리뷰트가 설정될 떄 특별한 기능을 수행해야 한다면, 애트리뷰트를 @property 데코레이터와 대응하는 setter 애트리뷰트로 옮겨갈 수 있다. # In[8]: 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 애트리뷰트를 변경된 전압 값에 맞춰 갱신한다. # In[9]: r2 = VoltageResistance(1e3) print(f'이전: {r2.current:.2f} 암페어') r2.voltage = 10 print(f'이후: {r2.current:.2f} 암페어') # 프로퍼티에 대해 setter를 지정하면 타입을 검사하거나 클래스 프로퍼티에 전달된 값에 대한 검증을 수행할 수 있다. # In[10]: 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 # 잘못된 저항 값을 대입하면 예외가 발생한다. # In[12]: r3 = BoundedResistance(1e3) # In[13]: r3.ohms = 0 # 생성자에 잘못된 값을 넘기는 경우에도 예외가 발생한다. # In[14]: BoundedResistance(-5) # 심지어 @property를 사용해 부모 클래스에 정의된 애트리뷰트를 불변으로 만들 수도 있다. # In[15]: 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 # In[16]: r4 = FixedResistance(1e3) # In[17]: r4.ohms = 2e3 # @property 메서드를 사용해 세터와 게터를 구현할 떄는 게터나 세터 구현이 예기치 않은 동작을 수행하지 않도록 만들어야 한다. # # 예를 들어 게터 프로퍼티 메서드 안에서 다른 애트리뷰트를 설정하면 안된다. # In[18]: 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 # In[19]: r7 = MysteriousResistor(10) r7.current = 0.01 print(f'이전: {r7.voltage:.2f}') r7.ohms print(f'이후: {r7.voltage:.2f}') # 게터나 세터를 정의할 때 가장 좋은 정책은 관려이 있는 객체 상태를 @property.setter 메서드 안에서만 변경하는 것이다. # # @property의 가장 큰 단점은 애트리뷰트를 처리하는 메서드가 하위 클래스 사이에서만 공유될 수 있다는 것이다. # ## 기억해야 할 내용 # - 새로운 클래스 인터페이스를 정의할 떄는 간단한 공개 애트리뷰트에서 시작하고, 세터나 게터 메서드를 가급적 사용하지 말라. # - 객체에 있는 애트리뷰트에 접근할 때 특별한 동작이 필요하면 @property로 이를 구현 할 수 있다. # - @property 메서드를 만들 때는 최소 놀람의 법칙을 따르고 이상한 부작용을 만들어 내지 말라 # - @property 메서드가 빠르게 실행되도록 유지하라. 느리거나 복잡한 작업의 경우(특히 I/O를 수행하는 등의 부수 효과가 있는 경우)에는 프로퍼티 대신 일반적인 메서드를 사용하라.