[코드잇] 쉽게 배우는 파이썬 문법 - 프로퍼티(Property) 5편

코드잇 / 20. 07. 07. 오후 2:02

안녕하세요, 온라인 코딩 스쿨 코드잇입니다.

오늘은 파이썬에서 프로퍼티(property)가 어떻게 작동하는지 그 원리를 배워볼게요. 프로퍼티의 원리를 배우기 위해 우리는 이전 글까지 파이썬의 디스크립터(descriptor)라는 걸 배웠습니다. 이 글을 이해하시려면 프로퍼티를 어떻게 사용하는지는 알고 계셔야 합니다. 혹시 잘 모르신다면 이전 글들을 읽고와주세요.

프로퍼티 1편

프로퍼티 2편

프로퍼티 3편

프로퍼티 4편

자, 프로퍼티가 무엇인지는 파이썬의 공식 문서에 있는 예를 약간 수정해서 설명해드릴게요.

일단 아래와 같은 Car 클래스가 있다고 합시다.

class Car:
    def __init__(self, initial_speed):
        self._speed = initial_speed

    @property
    def speed(self):
        '''I'm the 'speed' property.'''
        print('현재 속도 구하기')
        return self._speed

    @speed.setter
    def speed(self, value):
        print('현재 속도 설정하기')
        self._speed = value

    @speed.deleter
    def speed(self):
        print('현재 속도 정보 삭제하기')
        del self._speed


car = Car(50)
print(car.speed)
car.speed = 100
print(car.speed)
del car.speed

지금 Car 클래스는 _speed 라는 인스턴스 변수를 갖고 있습니다. 그리고 _speed 를 다루는 메소드들이 있는 speed라는 프로퍼티가 있죠. 위 코드를 실행하면 아래같은 결과가 출력됩니다.

현재 속도 구하기
50
현재 속도 설정하기
현재 속도 구하기
100
현재 속도 정보 삭제하기

프로퍼티가 존재할 때는 마치 그 프로퍼티와 같은 이름의 변수가 있는 것처럼 사용했을 때 각 상황에 알맞은 메소드가(getter, setter, deleter 중 하나) 호출된다고 이전 글에서 설명했습니다. 자, 여기까지 워밍업이었구요. 이제 왜 저렇게 쓰면 speed 라는 프로퍼티가 생기는지 알아봅시다.

파이썬 공식 문서에 있는 property의 코드는 다음과 같습니다.

class property(object):
    '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__)

일단, 윗 부분에 쓰인 'Emulate PyProperty_Type() in Objects/descrobject.c' 을 보면 property 의 실제 구현은 파이썬 코드가 아니라 파이썬을 실제로 해석해서 실행하는 인터프리터(이 인터프리터는 또다른 프로그래밍 언어인 C로 작성됨)에 존재한다는 걸 추측할 수 있겠죠? Cpython으로 구현된 프로퍼티의 실제 구현체를 보고 싶다면 이 링크를 참조하세요. 당장 이 말이 이해가 안 되어도 중요한 부분은 아니니까 넘어가셔도 괜찮습니다.

자, property 클래스의 코드를 볼까요?

property 클래스를 보니 이 클래스로 만들어진 객체는 결국 데이터 디스크립터(data descriptor)가 되겠네요. 왜냐하면 지금 __get__ 메소드, __set__ 메소드, __delete__ 메소드가 모두 정의되어 있으니까요!

다른 부분들은 무슨 뜻일까요? 방금 봤던 Car 클래스의 코드와 연관지어서 해석해봅시다.

일단 Car 클래스의 코드 중 이 코드 부분은

@property
def speed(self):
    '''I'm the 'speed' property.'''
    print('현재 속도 구하기')
    return self._speed

아래의 코드와 같은 뜻입니다.

def speed(self):
    '''I'm the 'speed' property.'''
    print('현재 속도 구하기')
    return self._speed

speed = property(speed)

이건 지금 speed 메소드 위에 붙은 @property 데코레이터를 해석한 결과입니다. 데코레이터가 어떻게 해석되는지 모르시는 분은 이전의 글을 참조하세요.

그럼 speed = property(speed) 부분이 실행될 때 어떤 일이 발생하는 걸까요? 지금 property 로 인스턴스를 만들고 있으니까 property 클래스의 __init__ 메소드 부분을 살펴보면 될 것 같네요. property 클래스의 __init__ 메소드 부분만 집중적으로 봐봅시다.

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__ 메소드에는 인스턴스 자신인 self 이외에도 fget, fset , fdel , doc 이라는 매개변수가 있죠? 그런데 모두 별다른 인스턴스를 지정하지 않으면 그냥 None 이라는 기본값으로 세팅이 되네요. 그런데 speed = property(speed) 코드를 보면 파라미터를 하나만 넣고 있네요? 이렇게 되면 저 4개의 파라미터 중 어느 파라미터로 전달되는 걸까요? 이런 경우에는 speed가 가장 첫번째인 fget 파라미터로 전달되고 나머지 3개의 파라미터는 그냥 None으로 세팅됩니다. 이건 메소드에 기본 인자(default argument)가 설정되었을 때의 규칙이 그래서 그런 겁니다.

정리해보면

Car 클래스에 있던 speed 라는 원래의 메소드는 프로퍼티 객체의 fget 인스턴스 변수가 가리키게 됩니다. 그 다음 그 프로퍼티 객체 자체의 이름이 이제부터 speed 가 되는 것이죠. 일단 여기까지는 이해하셨죠?

그럼 fget 변수가 가리키게 된 원래의 speed 메소드는 어디서 쓰이는 걸까요?

바로 property 클래스의 __get__ 메소드 내부에서 쓰입니다. __get__ 메소드 내부를 봅시다.

def __get__(self, obj, objtype=None):
    if obj is None:   #(1)
        return self
    if self.fget is None:   #(2)
        raise AttributeError('unreadable attribute')
    return self.fget(obj)    #(3)

어, 그런데 __get__ 메소드는 디스크립터의 값을 구하려고 할 때 호출되는 메소드 아니었나요? 맞습니다. 지금 주석을 하나씩 해보면

#1 : 프로퍼티를 사용하는 객체가 None으로 들어오면 프로퍼티 자체를 리턴하고

#2 : 만약 아직 프로퍼티의 fget 변수의 값이 None 이면 AttributeError 를 발생시킵니다.

사실 이 두 가지는 일반적이지 않은 예외 상황을 대비한 것이라 크게 중요하지는 않습니다. 핵심은 세 번째 주석에 있습니다.

#3 : 결국 정상적인 상황에서는 self.fget(obj) 이 호출됩니다. 그러니까 이 상황에서는 Car 클래스에 있었던 원래의 speed 메소드에 해당 Car 객체를 파라미터로 넣고 호출한다는 뜻입니다. 그럼 결국 Car 클래스의 이 메소드가 호출되는 거죠.

@property
def speed(self):
    '''I'm the 'speed' property.'''
    print('현재 속도 구하기')
    return self._speed

이제 프로퍼티가 어떻게 만들어지는지 감이 오시나요? 한 번 정리해볼게요.

1. Car 클래스에는 인스턴스 변수 _speed (언더바가 붙어있어요, 언더바 없는 speed 와 꼭 구별하세요)에 대한 getter 메소드 speed 가 있습니다.

2. 이 speed 라는 getter 메소드 위에 @property 데코레이터를 적어서, speed를 프로퍼티로 만듦과 동시에 프로퍼티 내부의 fget 은 원래의 speed getter 메소드를 가리키게 합니다.

3. 이렇게 한 후에, Car 클래스의 객체.speed 와 같이 마치 speed라는 변수가 Car 클래스에 있는 것처럼 호출하면.

4. speed 프로퍼티는 결국 파이썬의 디스크립터이기 때문에 그 안의 __get__ 메소드가 호출되고

5. __get__ 메소드의 내부를 보면 일반적인 상황에서는 self.fget(obj) 를 호출합니다. 그리고 이 fget은 2번 단계에서 말한 Car 클래스에 원래 있었던 speed getter 메소드입니다.

6. 원래의 speed 메소드는 이런 코드인데요. Car 클래스의 인스턴스 변수인 _speed 변수를 리턴해줍니다.

@property
def speed(self):
    '''I'm the 'speed' property.'''
    print('현재 속도 구하기')
    return self._speed

왜 이제 이 코드가 실행되면

car = Car(50)
print(car.speed)

이런 실행 결과가 나오는지 알겠죠?

현재 속도 구하기
50

프로퍼티의 작동원리가 이제 좀 이해되시나요?

그럼 이제 Car 클래스에서 _speed 변수에 대한 setter 메소드 speed 의 코드를 해석해볼까요?

@speed.setter
def speed(self, value):
    print('현재 속도 설정하기')
    self._speed = value

이 부분이 실행될 때는 이미 speed 는 메소드가 아니라 프로퍼티가 되어있는 상태입니다. 그렇기 때문에 위 코드는 아래와 같은 뜻입니다.

speed = speed.setter(_speed 변수의 setter 메소드)

이건 아까와 같은 원리니까 이해하기 쉽죠? 저 setter 메소드는 property 클래스 안에 있는 메소드입니다.

setter 메소드 부분을 보면

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

이렇게 되어있습니다. 그러니까 결국 _speed 변수의 setter 메소드fset 이라는 파라미터로 들어가게 됩니다. 이 코드에서 type(self) 는 무슨 뜻일까요? 이 코드는 실행되면 self로 들어온 인스턴스의 Type Object를 리턴하는데요. 이 말은 그 인스턴스의 클래스를 리턴한다는 뜻입니다. 일단 여기서는 property 클래스를 리턴한다고 생각하시면 됩니다. 그러니까 지금 이 코드는 아래 코드와 같은 뜻입니다.

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

이 코드가 실행되면 다시 property__init__ 메소드가 실행될 겁니다.

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
    self.fget = fget
    self.fset = fset    #!!! fset 변수 설정
    self.fdel = fdel
    if doc is None and fget is not None:
        doc = fget.__doc__
    self.__doc__ = doc

그럼 위 코드의 주석 내용대로 speed 프로퍼티의 fset 변수는 _speed 변수의 setter 메소드 를 가리키게 되겠죠?

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

주석 내용대로 해석해보면

#1 : 프로퍼티 객체의 fsetNone 이면 AttributeError 가 생기고

#2 : fsetNone 이 아니라면 self.fset(obj, value) 가 호출됩니다. 이 경우에는 설정된 _speed 변수의 setter 메소드 가 호출되겠죠? 바로 이 메소드 말입니다.

@speed.setter
def speed(self, value):
    print('현재 속도 설정하기')
    self._speed = value

그러니까

1. Car 클래스의 객체.speed = 100 이런 식으로 코드를 호출하면

2. speed 프로퍼티는 디스크립터라서 그 안의 __set__ 메소드가 호출되고

3. 그럼 결국 fset 이 가리키는 _speed 변수의 setter 메소드 가 호출되는 것이죠.

방금 봤던 코드들을 종합해서 보면

@property
def speed(self):
    '''I'm the 'speed' property.'''
    print('현재 속도 구하기')
    return self._speed

@speed.setter
def speed(self, value):
    print('현재 속도 설정하기')
    self._speed = value

왜 두 번째 데코레이터에는 @speed.setter 라고 적었는지 이제 아시겠죠? 이미 생성된 speed 라는 프로퍼티 객체의 fset 변수를 세팅해야하기 때문에 그런 겁니다.

마지막으로 Car 클래스의

@speed.deleter
def speed(self):
    print('현재 속도 정보 삭제하기')
    del self._speed

이 부분은 여러분이 해석해보시길 바랍니다. 방금 전에 설명했던 @speed.setter 부분과 같은 원리니까 어렵지 않을 겁니다~!

오늘은 프로퍼티가 어떻게 생성되는지 알아봤습니다. 데코레이터와 디스크립터에 대한 코드잇 블로그의 이전 글들을 읽어보셨다면 이해하기 어렵지 않으셨을 거에요. 혹시 그동안 프로퍼티를 그냥 사용만 해보시고 그 원리를 잘 모르셨던 분들에게 이 글이 좋은 설명이 되었기를 바랍니다.

기업문화 엿볼 때, 더팀스

로그인

/