파이썬 코딩의 기술 - 44

JinWooHyun·2021년 8월 16일
0

파이썬 코딩의 기술

목록 보기
14/14

setter와 getter 메서드 대신 평범한 애트리뷰트를 사용하라.

다른 언어를 사용하다 파이썬을 접한 프로그래머들은 클래스에 gettersetter 메서드를 명시적으로 정의하곤 한다.

class OldResistor:
    def __init__(self, ohms):
        self._ohms = ohms
        
   def get_ohms(self):
       return self._ohms
   
   def set_ohms(self, ohms):
       self._ohms = ohms

settergetter를 사용하기는 쉽지만, 이런 코드는 파이썬답지 않다.

r0 = OldResistor(50e3)
print('이전', r0.get_ohms())
r0.set_homs(10e3)
print('이후', r0.get_ohms())

>>>
이전: 50000.0
이후: 10000.0

특히 필드 값을 증가시키는 연산 등의 경우에 이런 메서드를 사용하면 코드가 지저분해진다.

r0.set_ohms(r0.get_homs() - 4e3)

하지만 이런 유틸리티 메서드를 사용하면 클래스 인터페이스를 설계할 때 도움이 되기도 한다. 즉 settergetter 같은 유틸리티 메서드를 쓰면 기능을 캡슐화하고, 필드 사용을 검증하고, 경계르 설정하기 쉬워진다. 클래스가 시간이 지남에 따라 진화하기 때문에 클래스를 설계할 때는 클래스를 호출하는 쪽에 영향을 미치지 않음을 보장하는 것이 중요하다.

하지만 파이썬에서는 명시적인 세터나 게터 메서드를 구현할 필요가 없다. 대신 다음과 같이 항상 단순한 공개 애트리뷰트로부터 구현을 시작할 수 있다.

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 애트리뷰트로 옮겨갈 수 있다.

다음은 Resistor의 새 하위 클래스를 만든다. Registor에서 voltage 프로퍼티에 값을 대입하면 current 값이 바뀐다. 코드가 제대로 작동하려면 settergetter의 이름이 우리가 의도한 프로퍼티 이름과 일치해야 한다.

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, homs):
        if ohms < 0:
            raise ValueError(f'저항 > 0 이어야 합니다. 실제 값 {ohms}')
        self._ohms = ohms

r3 = BoundedResistance(1e3)
r3.ohms = 0

BoundedResistance(-5)

>>>
Traceback ...
ValueError: 저항 > 0 이어야 합니다. 실제 값 0

Traceback ...
ValueError: 저항 > 0 이어야 합니다. 실제 값 -5

위 처럼 잘못된 저항값 대입, 생성자에 잘못된 값을 넘기는 경우 에러가 발생한다.
예외가 발생하는 이유는 BoundedResistance.__init__Resistor.__init__을 호출하고 이 초기화 메서드는 다시 self.ohms = -5라는 대입문을 실행하기 때문이다. 이 대입으로 인해 BoundedResistance에 있는 @ohms.setter 메서드가 호출되고, 이 setter 메서드는 객체 생성이 끝나기 전에 즉시 저항을 검증하는 코드를 실행한다.

심지어 @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("Omhs는 불변 객체입니다.")
        self._ohms = ohms

이 객체를 만든 다음 프로퍼티에 값을 대입하면 예외가 발생한다.

r4 = FixedResistnace(1e3)
r4.ohms = 2e3

>>>
Traceback ...
AttributeError: Ohms는 불변 객체입니다.

@property 메서드를 사용해 settergetter를 구현할 때는 예기치 않은 동작을 수행하지 않도록 만들어야 한다. 예를 들어 getter 프로퍼티 메서드 안에서 다른 애트리뷰트를 정의하면 안된다.

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

gettersetter를 정의할 때 가장 좋은 정책은 관련이 있는 객체 상태를 @property.setter 메서드 안에서만 변경하는 것이다. 동적으로 모듈을 임포트하거나, 아주 시간이 오래 걸리는 도우미 함수를 호출하거나, I/O를 수행하거나, 비용이 많이 드는 데이터베이스 질의를 수행하는 등 호출하는 쪽에서 예상할 수 없는 부작용을 만들어내면 안된다. 더 복잡하거나 느린 연산의 경우에는 일반적인 메서드를 사용하라.

@property의 가장 큰 단점은 애트리뷰트를 처리하는 메서드가 하위 클래스 사이에서만 공유될 수 있다는 것이다. 서로 관련이 없는 클래스 사이에 같은 구현을 공유할 수는 없다. 하지만 파이썬은 재사용 가능한 프로퍼티 로직을 구현할 때는 물론 다른 여러 용도에도 사용할 수 있는 descriptor 기능을 제공한다.

기억해야 할 내용

  • 새로운 클래스 인터페이스를 정의할 때는 간단한 공개 애트리뷰트에서 시작하고, 세터나 게터 메서드를 가급적 사용하지 말라.
  • 객체에 있는 애트리뷰트에 접근할 때 특별한 동작이 필요하면 @property로 이를 구현할 수 있다.
  • @property 메서드를 만들 때는 최소 놀람의 법칙을 따르고 이상한 부작용을 만들어내지 말라.
  • @property 메서드가 바르게 실행되도록 유지하라. 느리거나 복잡한 작업의 경우에는 프로퍼티 대신 일반적인 메서드를 사용하라.
profile
Unicorn Developer

0개의 댓글