Effective Python Item 44. Setter, Getter 대신 Attribute를 사용하라.

    들어가기 전

     


    요약

    • 새로운 클래스 인터페이스를 정의할 때, 간단히 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 구문을 추가하면 된다. 

    여기서 주의해서 보면 좋을 부분은 다음과 같다.

    1. BoundedResistance(1e3)이 실행.
    2. super().__init__(ohms)가 실행.
    3. self.ohms = ohms가 실행. → 이것은 @ohms.setter 함수를 실행
    4. super().__init__(ohms) 완료 
    5. 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를 불변으로 만들 수도 있다. 구현은 위와 같이 하면 되는데, 코드를 해석해보면 다음과 같다. 

    1. super().__init__(ohms)가 호출
    2. self.ohms = ohms가 호출 → @ohms.setter 함수가 호출. 
    3. @ohms.setter 함수에서 self는 FixedResistance임. 이 때, self에는 ohms라는 Attribute가 없음. self.ohms = ohms에 의해서 호출되었으므로, 아직까지 self.ohms가 초기화 되지 않은 상태이기 때문임. 따라서 hasattr()은 False로 처리되고, self._ohms = ohms가 호출됨. 
    4. 이후 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를 사용하는 사용자들을 혼란스럽게 만든다. 

    댓글

    Designed by JB FACTORY