[python] property는 어떻게 동작하는가

maintain·2020년 6월 14일
0

property를 이용한 자료구조를 구현하던 도중, 이 property가 어떻게 동작하는지 궁금해서 직접 파헤쳐 보았습니다. 우선 property의 구현 코드를 가져왔습니다.

구현 구조

   "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

공식 홈페이지에 있는 파이썬 구현 코드입니다. property는 기본적으로 데코레이터고, 위 구현 코드를 보면 클래스 데코레이터입니다.

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

먼저 매직 메서드 init은 단순히 인스턴스 변수에 값을 넣어주는 것 빼고는 하는 역할이 없습니다.

 def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

두번째로 매직 메서드 get, set, delete 세 가지는 인스턴스 변수에 저장된 함수를 실행하거나 인스턴스 변수가 정의되지 않았을 경우 에러 처리를 하는 역할만 하고 있습니다. 매직 메서드이므로 특수한 역할을 하고 있겠지만 이는 나중에 살펴보겠습니다.

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__) 

다음으로 getter, setter, deleter가 구현되어 있는데 type(self)를 통해 property 클래스를 반환하고 있습니다. 즉 getter, setter, deleter 어떤 것을 쓰든 전부 __init__을 통해 데코레이터를 만들고 있습니다. 여기까지 살펴본 결과 property의 역할을 하는 실제 코드는 작성되어 있지 않습니다. 그렇다면 property로서의 실제 역할은 매직 메서드인 __get__, __set__, __delete__의 기본 매커니즘이 하고 있는 게 분명합니다.

디스크립터

__get__, __set__, __delete__의 정체는 디스크립터입니다. 디스크립터의 공식 홈페이지 번역 일부를 빌리면 다음과 같습니다

어트리뷰트 액세스의 기본 동작은 객체의 딕셔너리에서 어트리뷰트를 가져오거나(get) 설정하거나(set) 삭제하는(delete) 것입니다. 예를 들어, a.x는 a.dict['x']로 시작한 다음 type(a).dict['x']를 거쳐, 메타 클래스를 제외한 type(a)의 베이스 클래스로 계속되는 조회 체인을 갖습니다. 조회된 값이 디스크립터 메서드 중 하나를 정의하는 객체이면, 파이썬은 기본 동작을 대체하고 대신 디스크립터 메서드를 호출 할 수 있습니다. 우선순위 체인에서 이것이 어디쯤 등장하는지는 어떤 디스크립터 메서드가 정의되었는지에 따라 다릅니다.

해석하면 객체에서 어트리뷰트를 호출할 때 인스턴스 -> 클래스 -> 부모 클래스 순으로 가는 호출 순서를 가지는 데, 이 단계에서 호출된 객체가 디스크립터 메서드를 정의하고 있을 경우 파이썬 기본 동작 대신 디스크립터 메소드를 실행한다는 내용입니다. 즉 디스크립터는 호출 시에 실행되는 메소드이고, 이 디스크립터를 정의함으로서 호출 시의 동작을 변경할 수 있는 것입니다. 또한 호출 동작에 따라서 단순 읽기/호출 시 __get__, 쓰기 시 __set__, 삭제 시 __delete__가 실행되게 됩니다.

이제 이 디스크립터가 propety에서 어떻게 동작했는지 살펴보겠습니다. 다음 코드를 예시로 들겠습니다.

class TestProperty:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        print(id(self))
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print('__get__ : ' + str(id(self)))
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        print("__set__ : " + str(id(self)))
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        print("__delete__ : " + str(id(self)))
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        print("__getter__")
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        print("setter in : " + str(id(self)))
        print("__setter__")
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print("__deleter__")
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

class test:
    def __init__(self, a):
        self.__a = a
        self.d = 0
        self._e = 0
        pass

    @TestProperty
    def a(self):
        return self.__a

    @a.setter
    def a(self, a):
        self.__a = a

    @a.getter
    def a(self):
        return self.__a

    @a.deleter
    def a(self):
        print('del')

A = test(3)
print(A.a)
A.a = 13
print(A.a)
del A.a

위 코드를 실행해보면 다음과 같습니다.

139993365239840
setter in : 139993365239840
__setter__
139993365239984
__getter__
139993365239840
__deleter__
139993365239984
__get__ : 139993365239984
3
__set__ : 139993365239984
__get__ : 139993365239984
13
__delete__ : 139993365239984
del

편의를 위해 id 가장 끝 두 자리만 나타내겠습니다.
맨 처음 기본 property만 사용하면 __init__의 첫 번째 인수가 fget이기 떄문에 자동적으로 self.fget이 정의됩니다. 이 때 만들어진 property 객체가 id 40입니다.

두 번쨰, setter를 통해이미 만들어진 self.fget을 포함한 property 인스턴스를 통해 self.fset이 추가된 새 property 인스턴스가 만들어집니다. 이 property 객체가 id 84 입니다.
이 때 새 인스턴스 id ~84가 id ~40을 덮어씌우면서 기존의 id ~40은 참조가 없어지므로 자동으로 삭제됩니다.

세 번째, getter를 통해 기존의 self.fget을 새로 fget으로 덮어씌운 property 인스턴스를 만듭니다. 이 때 삭제된 ~40의 자리를 그대로 사용하게 됩니다. 그리고 id ~40은 id ~84는 두 번째에서 보듯 참조가 없어져 삭제됩니다. id ~40은 id ~84를 덮어씌우면서 id ~84는 자동으로 삭제됩니다.

네 번째, deleter를 통해 기존의 self.fgetself.fset이 포함된 새 인스턴스를 만듭니다. 여기서도 세 번째처럼 삭제된 84의 id를 이용해서 새 인스턴스를 만들고 id ~40 이 삭제되고 id ~84 로 대체됩니다.
다섯 번째, property 인스턴스 id ~84 가 호출되고, __get__이 실행하면서 값 3을 반환하고 이것이 출력됩니다.

여섯 번째, property 인스턴스 id ~84 가 호출되고, __set__이 실행되면서 값 13이 저장되고 이것이 출력됩니다. 그리고 __get__이 실행되면서 13을 반환하고 이것이 출력됩니다.

일곱 번째, property 인스턴스 id ~84 가 호출되고, 삭제됩니다.

위의 과정을 요약하면, 맨 처음 기본적인 self.fget을 가진 property 인스턴스가 생성된 후, 이 인스턴스를 데코레이터로 직접 이용하여 __set__, __delete__를 설정하거나 수정할 때마다 기존 인스턴스가 가진 인스턴스 변수 정보를 이용해 새 인스턴스를 생성하는 방식으로 동작한다고 보면 되겠습니다.

마치며

이렇게 해서 property가 어떻게 동작하는지 알아냈습니다. 다만 여기서 계속 기존에 삭제된 id 참조를 재활용하고 있는데, 효율을 위한 내부 로직 때문으로 추측합니다. 이 부분도 파보고 싶지만 특성상 아주 깊은 내부로직을 보아야 할 것 같고, 그러기엔 제 수준이 모자른 것 같으니 다음에 기회가 된다면 파보도록 하겠습니다.

참고
파이썬 공식 홈페이지
https://docs.python.org/ko/3/howto/descriptor.html

0개의 댓글