[Effective Python] BW 44. 세터와 게터 메서드 대신 평범한 애트리뷰트를 사용하라

전민수·2023년 7월 12일
0

EffectivePython

목록 보기
9/10

이번 Better Way는 새로운 Chapter의 첫 번째 Better Way입니다. 이번 Chapter의 주요 개념인 메타클래스에 대해 정확히 공부하고자 서론을 작성하였습니다.

서론

메타클래스[1]

객체와 인스턴스

클래스 : 틀
객체 : 실체
흔히 객체 == 인스턴스

하지만 객체와 인스턴스는 어감상 차이가 있습니다.
객체는 실체 그 자체를 설명할 때
인스턴스는 틀과 실체 간의 관계를 설명할 때
적합합니다.

예시를 보면 직관적으로 와닿으실 겁니다.

minsu = Student()

1) Student는 클래스
2) minsu는 객체
3) minsu는 Student의 인스턴스

파이썬 'Everything is Object'

파이썬에서는 모든 것(부울, 정수, 실수, 문자열, 데이터 구조, 함수, 프로그램)이 객체(Object)로 구현되어 있다. … 파이썬 변수의 핵심을 살펴보자. 변수는 단지 이름일 뿐이다. 할당한다는 의미는 값을 복사하는 것이 아니다. 데이터가 담긴 객체에 그냥 이름을 붙이는 것이다. 그 이름은 객체 자신에 포함되는 것이라기보다는 객체의 참조다. 이름을 포스트잇처럼 생각하자.
(Introducing Python p.42-43)[2]

파이썬에서 모든 것은 객체입니다. 즉, 클래스도 객체입니다.
???
클래스로 생성한 게 객체라고 했잖아요?
그러게요. 말이 안되는 거 같나요?

그럼 이렇게 생각해봅시다.

붕어빵 틀로 붕어빵을 만듭니다. 이 때, 붕어빵 틀은 클래스고 붕어빵은 객체겠죠?

붕어빵 틀을 만다는 붕어빵 틀 주형 틀을 생각해봅시다.

이 때,

주형 틀 : 메타클래스
붕어빵 틀 : 클래스
라고 할 수 있습니다.

즉, 메타클래스는 '클래스를 인스턴스로 생성하는 클래스', '클래스의 클래스'라고 할 수 있습니다.

메타클래스의 디폴트는 정해져 있어, 우리가 특별히 재정의하지 않으면 디폴트 값으로, 우리가 지금까지 클래스를 쓰고 있는 방식으로 정해집니다.

하지만 클래스를 생성하는데 특별한 규칙을 만들고 싶으면 메타클래스를 재정의할 수 있습니다.
즉, 클래스의 동작을 더욱 섬세하게 제어하고 싶을 때, 메타클래스를 활용합니다.

애트리뷰트

: 클래스 내부에 포함돼 있는 메서드 나 변수

본론

C++로 OOP를 공부했다면 getter, setter method는 익숙할 것 입니다.
클래스의 멤버변수를 private로 선언하고, 유틸리티 메서드를 활용하여 오직 프로그래머의 의도대로 멤버변수에 접근하게 합니다.

이러한 방법의 이점은 다음과 같습니다.

1) 클래스의 캡슐화
2) 필드 사용 검증
3) 경계 설정 용이

코드로 보면 다음과 같습니다.

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

    def get_ohms(self):
        return self._ohms

    def set_ohms(self, ohms):
        self._ohms = ohms	# under score로 private attribute 선언

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

r0.set_ohms(r0.get_ohms() - 4e3)
assert r0.get_ohms() == 6e3

# 이전: 50000.0
# 이후: 10000.0

익숙한 방식이죠.
하지만 이 방식은 시각적 잡음이 많아 가독성이 떨어집니다.
그리고 코드 짜기 귀찮죠.
파이썬 답지 않습니다. Non_Pythonic!!!

파이썬에서의 private[3]

파이썬은 접근제어자(public, private 등)를 제공하지 않습니다. 대신 접근제어 '규칙'이 존재합니다. 애트리뷰트 이름 접두에 __(double under score)를 붙임으로써 private attribute를 선언합니다.

class OldResistor:
    def __init__(self, ohms):
        self.__ohms = ohms

r0 = OldResistor(50e3)
print('이전:', r0.__ohms)
r0.__ohms = 10e3
print('이후:', r0.__ohms)

r0.__ohms = r0.__ohms - 4e3
assert r0.__ohms == 6e3

# AttributeError: 'OldResistor' object has no attribute '__ohms'

하지만 이는 규칙, 즉 프로그래머 간의 관행인만큼 강제력이 부족합니다.

다음과 같이 _class__attributeName 으로 접근이 가능하죠.

class OldResistor:
    def __init__(self, ohms):
        self.__ohms = ohms

r0 = OldResistor(50e3)
print('이전:', r0._OldResistor__ohms)
r0._OldResistor__ohms = 10e3
print('이후:', r0._OldResistor__ohms)

r0._OldResistor__ohms = r0._OldResistor__ohms - 4e3
assert r0._OldResistor__ohms == 6e3

사실 double under score는 접근을 제한한다기 보다는 Name Mangling을 통해 애트리뷰트에 대한 접근을 어렵게 하는 것입니다.

Name Mangling : 컴파일러가 애트리뷰트의 이름을 짓이겨서 다른 이름으로 바꿔버리는 것을 말합니다.

즉, __ohms에서 __는 애트리뷰트 이름을 _OldResistor__ohms로 바꾸는 기능을 할 뿐이라고 할 수 있습니다.

파이썬의 default는 public

파이썬은 기본적으로 모든 속성이 public으로 처리되도록 설계됐습니다.
위의 내용은 이를 설명하기 위한 내용이라고 할 수 있습니다.

사실상 setter-getter 메서드를 구현해도 의미가 없다는 것입니다.

언어 자체가 이를 지양하는데 private를 쓸 필요가 없죠.

그래서 파이썬에서는 항상 단순한 공개(public) 애트리뷰트로부터 구현을 시작하는 것이 좋습니다.

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

애트리뷰트에 대해 확장된 기능을 구현하고 싶다면 @property 데코레이터 활용할 수 있습니다.

데코레이터 설명 참고 : https://velog.io/@setxty/Effective-Python-BW-26.-functools.wrap%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-%ED%95%A8%EC%88%98-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC

@property 데코레이터는 메서드를 변수처럼 쓸 수 있게 하며, setter 함수와 연결하여 할당 연산자를 통해 메서드를 실행할 수 있게 합니다.

이를 활용하면, 다음과 같은 기능을 구현할 수 있습니다.

1) 애트리뷰트가 설정될 때, 부가 기능 수행

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

위 코드에서 VoltageResitance 클래스는
(1) Resistor 클래스를 상속받고,
(2) voltage가 할당될 때 voltage setter 메서드가 호출되고,
(3) 변경된 전압 값에 따라 current 값을 계산
해줍니다.

r2 = VoltageResistance(1e3)
print(f'이전: {r2.current:.2f} 암페어')
r2.voltage = 10
print(f'이후: {r2.current:.2f} 암페어')

# 이전: 0.00 암페어
# 이전: 0.01 암페어

이와 같이, 변수가 설정될 때 실행되는 동작을 설정할 수 있습니다.

2) 타입 검사와 전달된 값에 대한 검증

property에 대한 setter의 내부를 설정하면 할당 연산자를 통해 할당되는 값에 대한 검증을 할 수 있습니다.
다음 코드는 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

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

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

또, 생성자에 잘못된 값을 넘기는 경우에도 예외가 발생합니다.

BoundedResistance(-5)

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

(1) BoundedResistance.__init__ 실행
(2) (1)에 의해 Resistor.__init__을 호출
(3) 이 초기화 매서드는 다시 self.ohms = -5라는 대입문을 실행

따라서 다음과 같은 예외가 발생합니다.

이를 이용하여, 코드가 개발자의 의도대로 동작하도록 변수 값을 제한할 수 있습니다.

3) 부모 클래스에 정의된 애트리뷰트를 불변으로 설정

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'):  # hasattr(name):object의 attribute 존재를 확인
            raise AttributeError("Ohms는 불변객체입니다")
        self._ohms = ohms

r4 = FixedResistance(1e3)

r4.ohms = 2e3
# AttributeError: Ohms는 불변객체입니다

이미 attribute가 존재할 경우, 예외를 발생시켜 변경하지 못하도록 할 수 있습니다.

최소 놀람의 법칙

@property를 사용해 세터와 게터를 구현할 때는 게터나 세터 구현이 예기치 않은 동작을 수행하지 않도록 해야 합니다.
예를 들어, 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

게터나 세터를 정의할 때 가장 좋은 정책은
관련이 있는 객체 상태를 @propery.setter 메서드 안에서'만' 변경하는 것입니다.

다음과 같은 동작은 지양하는 것이 좋습니다.
(1) high time-cost의 도우미 함수 호출
(2) I/O 수행
(3) high cost의 데이터베이스 질의를 수행
등등의 호출하는 쪽에서 예상할 수 없는 부작용을 만들어 내면 안됩니다.

일반적으로 getter-setter는 빠르고 사용하기 쉬울 것이라고 예상합니다.

@property의 단점

애트리뷰트를 처리하는 메서드가 하위 클래스 사이에서만 공유될 수 있다는 것입니다.
서로 관련 없는 클래스 간에는 같은 구현을 공유할 수 없습니다.

하지만 파이썬에서는 재사용 가능한 property 로직을 구현할 때는 물론 다른 용도에도 사용할 수 있는 디스크립터를 제공합니다.

Reference

[1] https://alphahackerhan.tistory.com/34
[2] 빌 루바노빅. 『처음 시작하는 파이썬(Introducing Python)』. 최길우(역). 한빛미디어, 2020.
[3] https://inkkim.github.io/python/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4%EC%9E%90%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC/

profile
Learning Mate

0개의 댓글