Python: @property

이걸 마지막으로, Python property 는 졸업했으면.

예전에도 같은 내용을 정리한 적이 있다. 우와, 벌써 6년 전이로군.
그동안 사실 Python 을 그다지 많이 파진 않았기에, 어찌보면 6년만에 처음으로 다시 이 개념에 대해 공부를 한 셈이 됐다.

‘개념’ 정리는 이 글로 끝이 될 수 있길 바라며..


Python Class 에서 쓰이는 @property 는, 그저 다른 언어에서 쓰이는 getter, setter, deleter 를 조금 쉽게 구현한 기능이다.
그리고 더더욱 중요한 사실. 이거 안써도 아무 관계가 없다.

요즘 열심히 읽고 있는 Beyond the Basic Stuff with Python(이 책은 물론이고, 이 책의 저자 Al Sweigart 책은 모두 맘에 든다. 이게 3권째인듯 한데, 모두 정말 내용이 알찼다.) p316 에 이런 표현이 있다.

I wish I could run some code each time this attribute was accessed, modified with an assignment statement, or deleted with a del statement.

우리말로 옮겨보자면,

“클래스 속성값이 참고되거나, 할당문에 의해 수정되거나, 또는 del 명령으로 삭제될 때마다 특정 코드가 실행됐으면 좋겠다.”

이런 바람이 있다면, 그 때가 바로 property 를 사용해야할 때라고 저자는 주장(?)하고 있다.
다른 OOP 언어들과 달리 Python 에서는 getter, setter 를 강제할 수 없다. 직접 속성값을 할당할 수 있기 때문이다.
그러나 이런 환경이더라도, property 를 사용하여 setter 메소드에 값을 검증해주는 코드를 넣는다면, 적어도 엉뚱한 값이 들어가는 상황은 막을 수가 있다.

아울러, Python Property 는 다음 상황하에서 ‘자동’으로 실행된다.

  • 코드에서 클래스 속성에 접근/참고할 때, 예를 들어 print(obj.someAttribute) 등이 실행되면, 자동으로 getter 메소드가 진행되고, 결과값이 출력된다.
  • 코드에서 클래스 속성에 새 값을 할당할 때, 예를 들어 obj.someAttribute = 'newValue' 이 실행되었다면 ‘newValue’ 값이 자동으로 setter 메소드로 넘어가고, 실행되어 결과값이 출력된다.
  • del 명령으로 속성을 삭제하면, 자동으로 deleter 메소드가 실행된다.

즉, 사용자가 따로 이 메소드들을 실행할 필요가 없다.
기본 밑밥은 여기까지.
이제, 어떻게 사용해야하는지, 그리고 주의사항은 뭔지 아래에 아주 간단하게 정리했다.


class ClassWithBadProperty:
    def __init__(self):
        self.someAttribute = 'some initial value'

    @property
    def someAttribute(self):  # getter
        return self._someAttribute

    @someAttribute.setter
    def someAttribute(self, value):  # setter
        self._someAttribute = value

Property 를 사용하려면, 먼저 @property 라는 문구를 넣고, 그 아래에 __init__ 에 사용한 변수명(Attribute)과 동일한 이름으로 메소드를 작성한다.
이 부분이 가장 중요하다. (어찌보면 당연하지만..)

또는, 이 부분이 좀 애매한데, 어떤 경우엔, init 에 쓰일 변수명에 ‘_변수명’ 형태를 쓰기도 한다. 즉, self._someAttribute 라고 하든, self.someAttribute 라고 하든 결과는 같다. (물론, 두 경우 모두 property 에 사용될 메소드 이름엔 밑줄이 빠져있어야 한다.)

위에서 init 메소드에 someAttribute 라는 이름으로 속성을 만들었다. 그렇다면, @property 에 사용될 메소드명은 반드시 someAttribute 여야만 한다. 만약 이름을 달리하면, (그런다고 오류가 발생되지는 않지만) 자동으로 getter 와 setter/deleter 가 작동하진 않는다.

다음 예를 보면 확실하다.

class ClassWithBadProperty1:
    def __init__(self):
        self.someAttribute_old = 'some initial value old'

    @property
    def someAttribute(self):  # getter
        print('getter1 찍고 갑니다.')
        return self._someAttribute

    @someAttribute.setter
    def someAttribute(self, value):  # setter
        print('Setter1 도 들렀다가')
        self._someAttribute = value

obj = ClassWithBadProperty1()
print(obj.someAttribute_old)

obj.someAttribute_old = '새 값이에요'
print(obj.someAttribute_old)

---------
some initial value old
새 값이에요

obj.someAttribute_old = '새 값이에요' 로 값을 할당했으므로, 제대로 설계됐다면 setter 가 작동했어야 하고, print 로 getter 도 실행됐어야 했지만, Attribute 값(someAttribute_old)과 메소드명(someAttribute)이 달랐기 때문에 그냥 무시돼 버렸다.

다음으로 주의해야 할 점이 있다.
반드시! 그리고 절대로, getter/setter/deleter 메소드에서 사용하는 변수를 속성명과 같게 하면 안된다!
이게 같아버리면 무한루프에 빠지는 현상이 나타난다.

class ClassWithBadProperty1:
    def __init__(self):
        self.someAttribute = 'some initial value'

    @property
    def someAttribute(self):  # getter
        print('getter1 찍고 갑니다.')
        return self.someAttribute

    @someAttribute.setter
    def someAttribute(self, value):  # setter
        print('Setter1 도 들렀다가')
        self.someAttribute = value

obj = ClassWithBadProperty1()
print(obj.someAttribute)

--------
Traceback (most recent call last):
  File "./badPropertyExample.py", line 45, in <module>
    obj = ClassWithBadProperty1()
  File "./badPropertyExample.py", line 25, in __init__
    self.someAttribute = 'some initial value old'
  File "./badPropertyExample.py", line 37, in someAttribute
    self.someAttribute = value
  File "./badPropertyExample.py", line 37, in someAttribute
    self.someAttribute = value
  File "./badPropertyExample.py", line 37, in someAttribute
    self.someAttribute = value
  [Previous line repeated 990 more times]
  File "./badPropertyExample.py", line 36, in someAttribute
    print('Setter1 도 들렀다가')
RecursionError: maximum recursion depth exceeded while calling a Python object

이런식으로 Recursion Error 가 발생한다.
이를 막기 위해서 property 에 사용하는 속성명은 보통 앞에 밑줄을 넣고, 뒤에 속성명을 넣는 형식을 취한다.

즉, self._someAttribute 가 된다.
그러나, 꼭 이렇게 써야 한다는 법은 없다. 밑줄을 써서 숨김 속성을 줄 필요도 없다. 각 프로퍼티에 쓰인 변수명만 같으면 된다.

class ClassWithBadProperty1:
    def __init__(self):
        self.someAttribute = 'some initial value old'

    @property
    def someAttribute(self):  # getter
        print('getter1 찍고 갑니다.')
        return self.bar

    @someAttribute.setter
    def someAttribute(self, value):  # setter
        print('Setter1 도 들렀다가')
        self.bar = value

print('property1 입니다.')
obj = ClassWithBadProperty1()
print(obj.someAttribute)
obj.someAttribute = '새 값이에요'
print(obj.someAttribute)

------
property1 입니다.
Setter1 도 들렀다가
getter1 찍고 갑니다.
some initial value old
Setter1 도 들렀다가
getter1 찍고 갑니다.
새 값이에요

이렇게, getter 와 setter 에 쓰인 변수명을 self.bar 식으로, init 에 쓰인 이름과 전혀 다르게 해도 아무 관계가 없다. (적어도 오류가 발생하진 않는다.) 다만, getter/setter/deleter 모두 같은 변수명을 써야 한다는 조건은 있다.

위의 경우, getter 에선 self.foo 를 쓰고 setter 에서 self.bar 를 썼다면 오류가 발생한다.

Traceback (most recent call last):
  File "./badPropertyExample.py", line 46, in <module>
    print(obj.someAttribute)
  File "./badPropertyExample.py", line 32, in someAttribute
    return self.foo
AttributeError: 'ClassWithBadProperty1' object has no attribute 'foo'

그러나!!

Python 관습으로, 보통 Property 에선 _Attribute 형식을 많이 쓰는 모양이다. 따라서, 그냥 이렇게 알고 있어야 남들이 쓴 코드를 이해할 수도, 내가 쓴 코드를 남이 알아보기도 쉽다.

마지막으로, 클래스의 속성(Attribute)은, 꼭 __init__ 에 먼저 정의될 필요는 없다. init 메소드에 변수를 정의하지 않아도, @property 를 사용해서 만들면 된다.

class ClassWithBadProperty2:
    def __init__(self):
        self.someAttribute = 'some initial value old'

    @property
    def someAttribute(self):
       --생략---

    @property
    def newAttribute(self):
        return self._newAttribute = 'tic_tac_toe'

이런 식으로, init 엔 없지만, property 를 써서 newAttribute 라는 속성을 만들어 줄 수도 있다.


** 추가.

Effective Python 에 멋있어 보이는 내용이 있어서 덧붙여본다.
이 책 Item 44, Use Plain Attributes Instead of Setter and Getter Methods (p183)에 Class Attr. 을 Immutable 로 만드는 편법(?)이 나와있다. @property 와 setter 를 사용한 조작(?)이다.

class Test1:
    def __init__(self, base_value):
        self.to_be_immutable = base_value

    @property
    def to_be_immutable(self):
        print('여기는 @property getter 입니다.')
        return self._to_be_immutable

    @to_be_immutable.setter
    def to_be_immutable(self, new_value):
        if hasattr(self, '_to_be_immutable'):
            raise AttributeError('Immutable 입니다!')
        self._to_be_immutable = new_value
        print('Setter 끝났음.')

그리고 실행하면..

In [26]: aa = Test1(55)
Setter 끝났음.

In [27]: aa.to_be_immutable
여기는 @property getter 입니다.
Out[27]: 55

In [28]: aa.to_be_immutable = 33
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-28-6f86f20ba986> in <module>
----> 1 aa.to_be_immutable = 33

~/Shared_Resilio/작업대/Effective Python/item45.py in to_be_immutable(self, new_value)
     14     def to_be_immutable(self, new_value):
     15         if hasattr(self, '_to_be_immutable'):
---> 16             raise AttributeError('Immutable 입니다!')
     17         self._to_be_immutable = new_value

AttributeError: Immutable 입니다!

_to_be_immutable 은 aa = Test1(55) 로 인스턴스를 생성할 때 이미 setter 가 실행된다. 정확히는, self.to_be_immutable = base_value 에서 setter 가 호출되고, 여기서 self.tobe_immutable 이 만들어진다.
따라서, aa.to_be_immutable = 33 에서 오류가 발생한다.

** 물론, aa._to_be_immutable 는 여전히 접근 가능. 즉, aa._to_be_immutable = 33 는 된다. 또, 이 두개체는 같은 개체다.

assert id(aa._to_be_immutable) == id(aa.to_be_immutable)

자.. 정리가 됐으려나?

Author: 아무도안

안녕하세요. 글 남겨주셔서 고맙습니다.