Python 강좌 : 제 38강 - 속성(Property)
속성(Property)
속성(Property)은 클래스에서 멤버의 값을 읽거나 쓸 수 있도록 제공하는 방식입니다.
클래스의 멤버를 직접 호출해 읽거나 변경할 수 있지만, 속성(Property)를 사용하면, 안정성과 유연성을 향상시킬 수 있습니다.
멤버를 은닉시켜 속성의 직접 접근을 막는다면, 안정성을 향상시킵니다.
멤버를 직접변경하는 방식이 아닌, 속성(Property)를 통해 변경하면 추가적인 작업을 진행할 수 있어 유연성을 향상시킵니다.
Property는 겉으로는 instance.value처럼 일반 속성과 똑같이 읽고 쓰지만, 실제로는 그때마다 정의해 둔 메서드가 호출되는 속성입니다. 즉, 사용하는 쪽은 속성처럼 간단하게 쓰고, 클래스 내부에서는 메서드처럼 검사나 계산을 처리할 수 있습니다.
이번 강좌에서는 Property가 왜 필요한지 먼저 살펴본 다음, 이름 장식(Name Mangling)을 이용한 비공개 속성과 Getter, Setter, Deleter의 사용법, 그리고 값 검증과 계산된 속성을 다룹니다.
- Tip : 제 37강의
속성(Attribute)은 객체에.으로 접근하는 모든 이름을 의미하며, 이번 강좌의속성(Property)은 그중 메서드로 동작하는 특별한 속성을 의미합니다.
Property가 필요한 이유
처음 클래스를 설계할 때는 대부분 self.age = age처럼 일반 속성으로 값을 저장합니다.
그런데 프로그램을 사용하다 보면 나이에 음수가 들어가는 것처럼, 나중에 값을 검사해야 할 필요가 생깁니다.
다른 언어에서는 이를 대비해 처음부터 get_age(), set_age() 메서드를 만들어 두는 경우가 많지만, Python에서는 일반 속성으로 시작하고 필요해질 때 Property로 바꾸는 방식을 사용합니다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("윤대희", 20)
person.age = 21
print(person.age)
person.age = -5
print(person.age)- 결과
- 21
-5
첫 번째 Person은 age를 일반 속성으로 저장합니다.
person.age = 21과 print(person.age)처럼 간단하게 사용할 수 있지만, person.age = -5처럼 잘못된 값을 할당해도 막을 방법이 없어 -5가 그대로 저장됩니다.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("나이는 0 이상이어야 합니다.")
self._age = value
person = Person("윤대희", 20)
person.age = 21
print(person.age)
try:
person.age = -5
except ValueError as e:
print("ValueError:", e)
print(person.age)- 결과
- 21
ValueError: 나이는 0 이상이어야 합니다.
21
두 번째 Person은 클래스 내부만 수정해 age를 Property로 바꿨습니다.
중요한 점은 클래스를 사용하는 코드인 person.age = 21, print(person.age), person.age = -5가 첫 번째 예제와 한 글자도 바뀌지 않았다는 것입니다.
person.age = 21은 이제 Setter인 age(self, value) 메서드를 호출해 값을 검사한 뒤 _age에 저장하고, person.age는 Getter가 _age를 반환합니다. -5를 할당하면 ValueError가 발생하고, 값은 이전의 21로 유지됩니다.
만약 처음부터 일반 속성을 get_age(), set_age() 메서드로 바꿔야 했다면, 이 클래스를 사용하는 모든 코드를 person.set_age(21)처럼 고쳐야 했을 것입니다.
Property 덕분에 Python에서는 처음에 단순한 일반 속성으로 작성하고, 검증이나 계산이 필요해졌을 때 호출부를 바꾸지 않고 내부 구현만 바꿀 수 있습니다.
Private & Property
class Daeheeyun:
def __init__(self):
self.__value = 0.76923
@property
def value(self):
return self.__value
instance = Daeheeyun()
print(instance.value)- 결과
- 0.76923
실제 값은 __value라는 비공개 이름에 저장하고, 외부에는 value라는 Property로만 공개하는 예제입니다.
instance.value는 괄호 없이 접근했지만 value 메서드가 호출되어 __value의 값인 0.76923이 반환됩니다.
Private
변수명 앞에 이중 밑줄(Double Underscore)을 붙이면, 외부에서 쉽게 접근하지 못하는 비공개(Private) 속성처럼 사용할 수 있습니다.
다른 언어에서는 public, private 같은 접근 한정자로 접근 수준을 제한합니다.
public은 어디에서나 접근이 가능하며, private는 해당 클래스 내부에서만 접근이 가능합니다.
하지만 Python에는 실제로 접근을 막는 접근 한정자가 없으며, 이중 밑줄은 이름을 바꾸는 이름 장식(Name Mangling)으로 동작합니다.
class Daeheeyun:
def __init__(self):
self.__value = 0.76923
instance = Daeheeyun()
try:
print(instance.__value)
except AttributeError as e:
print("AttributeError:", e)
print(instance.__dict__)
print(instance._Daeheeyun__value)- 결과
- AttributeError: ‘Daeheeyun’ object has no attribute ‘__value’
{‘_Daeheeyun__value’: 0.76923}
0.76923
클래스 내부에서 작성한 self.__value는 self._Daeheeyun__value처럼 _클래스명이 앞에 붙은 이름으로 저장됩니다.
그러므로 instance.__value로 외부에서 접근할 경우, 해당 이름의 속성이 없어 오류가 발생합니다.
단, instance._Daeheeyun__value처럼 바뀐 이름을 사용하면 접근할 수 있으므로, 완전히 숨겨지는 것은 아닙니다.
이름 장식은 자식 클래스가 같은 이름의 속성을 정의해도 부모 클래스의 속성과 충돌하지 않도록 하는 데 주로 사용합니다.
이름 장식으로 값을 감추고 Property로만 공개하면, 클래스의 속성값이 허용 가능한 범위에서만 변경이 가능하도록 제어할 수 있습니다.
- Tip : 관례적으로
단일 밑줄(Single Underscore)로 시작하는 이름(_value)은 내부용이라는 의미로 사용합니다. 단일 밑줄로 정의된 클래스나 변수는from <module> import *형태로import할 때 불러와지지 않습니다. (__all__이 정의되지 않은 경우)
class Parent:
def __init__(self):
self.__id = "부모"
self._name = "부모"
def parent_id(self):
return self.__id
class Child(Parent):
def __init__(self):
super().__init__()
self.__id = "자식"
self._name = "자식"
child = Child()
print(child.__dict__)
print(child.parent_id())- 결과
- {‘_Parent__id’: ‘부모’, ‘_name’: ‘자식’, ‘_Child__id’: ‘자식’}
부모
이름 장식이 실제로 어떤 문제를 막는지 보여주는 예제입니다.
Parent의 self.__id는 _Parent__id로, Child의 self.__id는 _Child__id로 저장되므로, 같은 __id라는 이름을 사용했지만 서로 다른 속성이 됩니다. 그러므로 부모 클래스의 메서드인 parent_id()는 여전히 부모를 반환합니다.
반면 단일 밑줄의 _name은 이름이 바뀌지 않으므로, 자식 클래스의 할당이 부모 클래스의 값을 덮어써 자식만 남습니다.
그러므로 단순히 “내부용”이라는 표시가 필요하다면 단일 밑줄을, 상속받는 클래스와 이름이 충돌하면 안 되는 경우에만 이중 밑줄을 사용합니다.
Property
속성(Property)은 메서드명 위에 @property를 통해 속성으로 할당할 수 있습니다.
속성으로 할당된 메서드는 하나의 인수(self)만 사용할 수 있습니다.
메서드처럼 구현되었지만, 속성을 호출하는 방식처럼 instance.value로 사용합니다.
@property, value는 기본적으로 속성 값을 반환하는 getter가 됩니다.
@property는 메서드를 property 객체로 감싸 클래스 속성 value에 저장합니다. 제 37강의 속성 조회 순서에서 살펴본 것처럼, property는 인스턴스의 __dict__보다 먼저 확인되므로 instance.value에 접근할 때마다 메서드가 호출됩니다.
-
Tip :
@는데코레이터(Decorator)를 의미합니다. 데코레이터는 제 47강에서 자세히 다룹니다. -
Tip :
instance.value()로 사용할 경우,instance.value의 값인0.76923을 호출하게 되어TypeError: 'float' object is not callable오류를 발생시킵니다.
Getter & Setter & Deleter
class Daeheeyun:
def __init__(self):
self.__value = 0.76923
@property
def value(self):
pass
@value.getter
def value(self):
print("GET")
return self.__value
@value.setter
def value(self, value):
print("SET")
self.__value = value
@value.deleter
def value(self):
print("DEL")
del self.__value
instance = Daeheeyun()
instance.value = 100
print(instance.value)
del instance.value- 결과
- SET
GET
100
DEL
Property는 값을 읽기·쓰기·제거하는 세 가지 동작에 각각 메서드를 연결할 수 있습니다.
결과를 순서대로 읽으면, instance.value = 100은 Setter를 호출해 SET을 출력하고, print(instance.value)는 Getter를 호출해 GET을 출력한 뒤 100을 반환합니다. 마지막으로 del instance.value는 Deleter를 호출해 DEL을 출력하고 __value를 제거합니다.
| 문법 | 호출되는 메서드 | 데코레이터 |
|---|---|---|
instance.value |
Getter |
@property 또는 @value.getter |
instance.value = 값 |
Setter |
@value.setter |
del instance.value |
Deleter |
@value.deleter |
Getter
Getter는 속성의 값을 호출할 때 발생합니다.
value.getter를 사용하지 않는다면, @property가 Getter가 됩니다.
일반적으로는 예제처럼 @value.getter를 따로 작성하지 않고, @property로 정의한 메서드를 Getter로 사용합니다.
예제의 @property로 정의한 value는 pass만 실행하므로, @value.getter로 다시 정의하지 않았다면 instance.value는 None을 반환했을 것입니다.
속성을 호출했을 때에도 추가적인 프로세스를 가질 수 있습니다.
Setter
Setter는 속성의 값을 할당할 때 발생합니다.
value.setter를 사용하지 않는다면, 값을 변경할 수 없습니다.
Setter로 할당된 메서드는 두 개의 인수(self, value)만 사용할 수 있습니다.
Getter, Setter, Deleter의 메서드 이름은 모두 속성의 이름인 value와 같아야 합니다.
@value.setter는 기존 value 속성을 수정하는 것이 아니라, 기존 Getter에 Setter를 추가한 새로운 property 객체를 만들어 반환합니다. 메서드 이름이 같아야 이 새 객체가 value라는 이름에 다시 저장됩니다.
class Daeheeyun:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def set_value(self, value):
self._value = value
instance = Daeheeyun()
try:
instance.value = 10
except AttributeError as e:
print("AttributeError:", e)
instance.set_value = 10
print(instance.value, instance.set_value)- 결과
- AttributeError: property ‘value’ of ‘Daeheeyun’ object has no setter
10 10
Setter의 이름을 set_value처럼 다르게 작성하면 자주 발생하는 실수입니다.
@value.setter가 만든 새 property 객체가 set_value라는 다른 이름에 저장되므로, 원래의 value는 여전히 Setter가 없는 읽기 전용 속성으로 남아 AttributeError가 발생합니다.
대신 set_value가 Getter와 Setter를 모두 가진 별도의 속성이 되어, instance.set_value = 10은 정상적으로 동작합니다.
Deleter
Deleter는 속성의 값을 제거할 때 발생합니다.
value.deleter를 사용하지 않는다면, 값을 제거할 수 없습니다.
Deleter는 캐시를 비우거나 연결된 자원을 정리하는 것처럼, 속성을 제거할 때 함께 처리해야 할 작업이 있을 때 정의합니다. 실제로는 Getter와 Setter에 비해 사용 빈도가 낮습니다.
property() 함수
class Daeheeyun:
def __init__(self):
self._value = 0
def get_value(self):
return self._value
def set_value(self, value):
self._value = value
value = property(get_value, set_value, doc="값 속성")
instance = Daeheeyun()
instance.value = 10
print(instance.value)
print(type(Daeheeyun.value))
print(Daeheeyun.value.__doc__)- 결과
- 10
<class ‘property’>
값 속성
property(fget, fset, fdel, doc)처럼 데코레이터 없이 property() 함수에 메서드를 직접 전달해도 같은 속성을 만들 수 있습니다.
type(Daeheeyun.value)가 <class 'property'>인 것처럼, Property는 클래스에 저장된 property 객체입니다. 클래스를 통해 접근하면 메서드가 호출되지 않고 property 객체 자체가 반환됩니다.
데코레이터 방식은 이 함수를 더 읽기 쉽게 작성하는 방법이며, 일반적으로는 데코레이터 방식을 사용합니다.
읽기 전용 속성
class Daeheeyun:
def __init__(self):
self.__value = 0.76923
@property
def value(self):
return self.__value
instance = Daeheeyun()
try:
instance.value = 100
except AttributeError as e:
print("AttributeError:", e)
try:
del instance.value
except AttributeError as e:
print("AttributeError:", e)- 결과
- AttributeError: property ‘value’ of ‘Daeheeyun’ object has no setter
AttributeError: property ‘value’ of ‘Daeheeyun’ object has no deleter
@property만 정의하고 Setter와 Deleter를 정의하지 않으면, 읽기 전용 속성이 됩니다.
값을 할당하거나 제거하면 AttributeError가 발생합니다.
오류 메시지에 has no setter, has no deleter가 표시되므로, 어떤 동작이 정의되지 않았는지 바로 알 수 있습니다.
읽기 전용 속성은 인스턴스를 만든 뒤 바뀌면 안 되는 값(식별 번호, 생성 시각 등)을 외부에 공개할 때 사용합니다. 단, 클래스 내부에서는 self.__value를 통해 여전히 값을 변경할 수 있습니다.
값 검증과 계산된 속성
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("절대 영도보다 낮을 수 없습니다.")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
temp = Temperature(25)
print(temp.fahrenheit)
try:
temp.celsius = -300
except ValueError as e:
print("ValueError:", e)
print(temp.celsius)- 결과
- 77.0
ValueError: 절대 영도보다 낮을 수 없습니다.
25
Setter에서 값을 검사하면, 허용 가능한 범위의 값만 할당되도록 제어할 수 있습니다.
__init__의 self.celsius = celsius도 Setter를 거치므로, 인스턴스를 생성할 때도 값이 검사됩니다.
fahrenheit처럼 다른 속성으로부터 계산되는 값을 속성으로 제공할 수도 있습니다. Setter가 없으므로 읽기 전용입니다.
temp.fahrenheit는 접근할 때마다 _celsius로부터 다시 계산되므로, celsius를 바꾸면 fahrenheit도 자동으로 바뀝니다. 화씨 온도를 별도의 속성으로 저장했다면 두 값을 함께 갱신해야 하지만, 계산된 속성은 값이 어긋날 일이 없습니다.
-300을 할당하면 Setter에서 ValueError가 발생하므로, _celsius는 이전 값인 25로 유지됩니다.
실제 값은 _celsius에 저장하며, 속성 이름인 celsius와 다른 이름을 사용해야 Setter가 자기 자신을 다시 호출하지 않습니다.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self.celsius
@celsius.setter
def celsius(self, value):
self.celsius = value
try:
Temperature(25)
except RecursionError:
print("RecursionError 발생")- 결과
- RecursionError 발생
Setter 안에서 self.celsius = value로 속성과 같은 이름에 할당하면, 그 할당이 다시 Setter를 호출합니다.
호출이 끝없이 반복되어 RecursionError가 발생하므로, 실제 값은 반드시 _celsius처럼 다른 이름에 저장해야 합니다. Getter의 return self.celsius도 같은 이유로 무한 재귀가 됩니다.
from functools import cached_property
class Report:
def __init__(self, numbers):
self.numbers = numbers
@cached_property
def total(self):
print("합계 계산")
return sum(self.numbers)
report = Report([1, 2, 3])
print(report.total)
print(report.total)
print(report.__dict__)- 결과
- 합계 계산
6
6
{‘numbers’: [1, 2, 3], ‘total’: 6}
Python 3.8 이상의 functools.cached_property는 처음 접근할 때만 메서드를 실행하고, 그 결과를 저장해 두는 속성입니다.
첫 번째 report.total에서만 합계 계산이 출력되고, 두 번째부터는 저장된 6을 바로 반환합니다.
결과는 인스턴스의 __dict__에 total이라는 이름으로 저장되며, 이후에는 이 값이 일반 인스턴스 속성처럼 사용됩니다.
property는 접근할 때마다 다시 계산하므로 항상 최신 값을 반환하고, cached_property는 한 번만 계산하므로 빠르지만 numbers가 바뀌어도 결과가 갱신되지 않습니다. 그러므로 값이 바뀌지 않는 데이터에서 계산 비용이 클 때만 cached_property를 사용합니다.
- Tip : 계산 비용이 큰 값을 한 번만 계산해 저장하려면,
Python 3.8이상의functools.cached_property를 사용할 수 있습니다. 저장된 값을 지우고 다시 계산하려면del report.total을 실행합니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
__이름 |
이름 장식(Name Mangling) | _클래스명__이름으로 저장, 상속 시 이름 충돌 방지 |
_이름 |
내부용 관례 | 이름이 바뀌지 않음, 접근 제한 없음 |
@property |
Getter 정의 |
instance.이름으로 읽을 때 호출 |
@이름.setter |
Setter 정의 |
instance.이름 = 값으로 할당할 때 호출, 메서드 이름이 같아야 함 |
@이름.deleter |
Deleter 정의 |
del instance.이름으로 제거할 때 호출 |
property(fget, fset, fdel, doc) |
함수 방식 | 데코레이터 방식과 같은 property 객체 |
functools.cached_property |
한 번만 계산 (Python 3.8 이상) |
결과를 인스턴스 __dict__에 저장 |
Property를 사용하면 일반 속성으로 시작한 클래스에 호출부를 바꾸지 않고 검증과 계산을 추가할 수 있습니다.
실제 값은 _이름이나 __이름처럼 속성과 다른 이름에 저장해야 Getter와 Setter가 자기 자신을 다시 호출하지 않습니다.
댓글 남기기