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

코드잇 / 조회수 : 4447

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

​

오늘은 파이썬에서 프로퍼티(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 : 프로퍼티 객체의 fset 이 None 이면 AttributeError 가 생기고

#2 : fset 이 None 이 아니라면 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 부분과 같은 원리니까 어렵지 않을 겁니다~!

​

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

​

​

기업문화 엿볼 때, 더팀스

로그인

/