파이썬 코딩의 기술 - 42

JinWooHyun·2021년 8월 7일
0

파이썬 코딩의 기술

목록 보기
12/14

비공개 애트리뷰트보다는 공개 애트리뷰트를 사용하라

파이썬에서 클래스의 애트리뷰트는 publicprivate 두 가지가 있다.

class MyObject:
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

    def get_private_field(self):
        return self.__private_field

foo = MyObject()
assert foo.public_filed == 5

애트리뷰트 이름 앞에 밑줄을 두 개(__) 붙이면 비공개 필드가 된다. 비공개 필드를 포함하는 클래스 안에 있는 메서드에서는 해당 필드에 직접 접근할 수 있다.

assert foo.get_private_field() == 10

하지만 클래스 외부에서 비공개 필드에 접근하면 예외가 발생한다.

foo.__private_field

>>>
Error!

클래스 메서드는 자신을 둘러싸고 있는 class 블록 내부에 들어 있기 떄문에 해당 클래스의 비공개 필드에 접근할 수 있다.

class MyOtherObject:
    def __init__(self):
        self.__private_field = 71
    
    @classmethod
    def get_private_field_of_instance(cls, instance):
        return instance.__private_field 
        
bar = MyOtherObject()
assert MyOtherObject.get_private_field_of_instance(bar) == 71

하위 클래스는 부모 클래스의 비공개 필드에 접근할 수 없다.

class MyParentObject:
    def __init__(self):
        self.__private_field = 71
    
class MyChildObject(MyParentObject):
    def get_private_field(self):
        return self.__private_field

baz = MyChildObject()
baz.get_private_field()

>>>
error!

비공개 애트리뷰트의 동작은 애트리뷰트 이름을 바꾸는 단순한 방식으로 구현된다. MyChildObject.get_private_field 처럼 메서드 내부에서 비공개 애트리뷰트에 접근하는 코드가 있으면, 파이썬 컴파일러는 __private_field라는 애트리뷰트 접근 코드를 _MyChildObject__private_field라는 이름으로 바꿔준다.

위 예제에서 MyParentObject.__init__ 안에만 __private_field 정의가 들어 있다. 이는 이 비공개 필드의 이름이 실제로는 _MyParentObject__private_field라는 뜻이다. 부모의 비공개 애트리뷰트를 자식 애트리뷰트에서 접근하면, 변경한 애트리뷰트 이름 _MyParentObject__private_field이 아니라 _MyChildObject__private_field로 존재하지 않는다는 오류가 발생한다.

이 때문에 하위 클래스에서든 클래스 외부에서든 원하는 클래스의 비공개 애트리뷰트에 접근할 수 있다.

assert baz._MyParentObject__private_field == 71

객체 애트리뷰트 딕서너리를 살펴보면 실제 변환된 비공개 애트리뷰트 이름이 들어 있는 것을 볼 수 있다.

print(baz.__dict__)

>>>
{'_MyParentObject__private_field': 71}

비공개 애트리뷰트에 대한 버근 구문이 실제로 가시성을 엄격하게 제한하지 않는 이유는, 파이썬은 "우리가 하고 싶은 일을 언어가 제한하면 안된다" 라는 생각 속에 만들어졌기 때문이다. 게다가 파이썬은 애트리뷰트에 접근할 수 있는 언어 기능에 대한 훅(__getattr__, __setattr__)을 제공하기 떄문에 원할 경우에는 객체 내부를 마음대로 접근할 수 있다.

내부에 몰래 접근함으로써 생길 수 있는 피해를 줄이고자 파이썬 프로그래머는 스타일 가이드에 정해진 명명 규약을 지킨다. 필드 앞에 밑줄이 하나만 있으면 (_protected_field) 관례적으로 protected 필드를 뜻한다. protected 필드는 클래스 외부에서 이 필드를 사용하는 경우 주의하라는 뜻이다.

파이썬을 처음 사용하는 많은 프로그래머가 하위 클래스나 클래스 외부에서 사용하면 안 되는 내부 API를 표현하기 위해 비공개 필드를 사용한다.

class MyStringClass:
    def __init__(self, value):
        self.__value = value
    
    def get_value(self):
        return str(self.__value)

foo = MyStringClass(5)
assert foo.get_value() == '5'

이런 방법은 잘못된 것이다. 누군가 이 클래스를 상속하면서 새로운 기능을 추가하거나 기존 메서드의 단점을 해결하기 위해 새로운 동작을 추가하길 원할 수 있다. 비공개 애트리뷰트를 사용하면 이런 확장이나 하위 클래스의 오버라이드를 어렵게 하고 깨지기 쉽게 만든다. 하위 클래스에서 비공개 필드에 꼭 접근해야 한다면 여전히 비공개 필드에 접근할 수 있다.

class MyIntegerSubclass(MyStringClass):
    def get_value(self):
        return int(self._MyStringClass__value)

foo = MyIntegerSubClass('5')
assert foo.get_value() == 5

하지만 클래스 정의를 변경하면 더 이상 비공개 애트리뷰트에 대한 참조가 바르지 않으므로 하위 클래스가 깨질 것이다.

class MyBaseClass:
    def __init__(self, value):
        self.__value = value

    def get_value(self):
        return self.__value

class MyStringClass(MyBaseClass):
    def get_value(self):
        return str(super().get_value()) # 변경됨

class MyIntegerSubClass(MyStringClass):
    def get_value(self):
        return int(self._MyStringClass__value) # 변경되지 않음, 깨짐
        

일반적으로 상속을 허용하는 클래스(부모) 쪽에서 protected 애트리뷰트를 사용하고 오류를 내는 편이 낫다. 모든 protected 필드에 문서를 추가한 후, API 내부에 있는 필드 중에서 어떤 필드를 하위 클래스에서 변경할 수 있고 어떤 필드는 그대로 놔둬야 하는지 명시해야 한다.

class MyStringClass:
    def __init__(self, value):
        # 여기서 객체에게 사용자가 제공한 값을 저장한다
        # 사용자가 제공하는 값은 문자열로 타입 변환이 가능해야 하며
        # 일단 한번 객체 내부에 설정되고 나면 불변 값으로 취급돼야 한다
        self._value = value

비공개 애트리뷰트를 사용할지 고민해야 하는 경우는 하위 클래스의 필드와 이름이 충돌할 수 있는 경우뿐이다. 자식 클래스가 실수로 부모 클래스가 이미 정의한 애트리뷰트를 정의하면 충돌이 생길 수 있다.

class ApiClass:
    def __init__(self):
        self._value = 5
    
    def get(self):
        return self._value

class Child(ApiClass):
    def __init__(self):
        super().__init__()
        self._value = 'hello' # 충돌

주로 공개 API에 속한 클래스의 경우 신경 써야 하는 부분이다. 이런 문제가 발생할 위험을 줄이려면, 부모 클래스 쪽에서 자식 클래스의 애트리뷰트 이름이 자신의 애트리뷰트 이름과 겹치는 일을 방지하기 위해 비공개 애트리뷰트를 사용할 수 있다.

class ApiClass:
    def __init__(self):
        self.__value = 5
    
    def get(self):
        return self.__value

class Child(ApiClass):
    def __init__(self):
        super().__init__()
        self._value = 'hello'

a = Child()
print(a.get())
print(a._value)

>>>
5
hello

기억해야 할 내용

  • 파이썬 컴파일러는 비공개 애트리뷰트를 자식 클래스나 클래스 외부에서 사용하지 못하도록 엄격히 금지하지 않는다.
  • 비공개 애트리뷰트로 접근을 막으려고 시도하기보다는 보호된 필드를 사용하면서 문서에 적절한 가이드를 남겨라
  • 코드 작성을 제어할 수 없는 하위 클래스에서 이름 충돌이 일어나는 경우를 막고 싶을 때만 비공개 애트리뷰트를 사용할 것을 권한다.
profile
Unicorn Developer

0개의 댓글