이걸 마지막으로, 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)
자.. 정리가 됐으려나?