Python
Python - Property
foxlee
2024. 2. 19. 21:35
1. 사전 지식
- 파이썬 클래스
- 데코레이터
- 클래스로 코드를 작성한 경험 및 인스턴스의 속성(상태)을 다루어본 경험
2. Property은 무엇이고 장점은 무엇이고 항상 써야하는가?
- Property
- Python의 property()를 사용하면 클래스에서 속성(상태)을 생성할 수 있음
- 안정적인 API를 제공하면서 클래스와 객체에 의존하는 사용자의 코드를 깨뜨리지 않고도 코드 변경을 관리할 수 있음(=코드의 유지보수성과 확장성 향상)
- 일반적인 케이스(아래 예시와 같은)는 인스턴스 속성을 접근/수정/삭제 하는 것임(클래스 속성이 아님)
- 장점
- 속성을 정의하는 데 사용되는 문법은 매우 간결하고 읽기 쉬움
- 인스턴스 속성에 직접 접근하거나 수정하는 것을 피하면서도, 마치 공개 속성처럼 정확하게 접근할 수 있으며, 새로운 값의 유효성을 검사할 수 있음(예로 사용자는 .name를 접근/수정하지만 클래스 내부에서는._name을 관리함)
- @property를 사용하면 속성의 이름을 "재사용"하여 게터(접근), 세터(수정), 삭제에 대해 새로운 이름을 생성하지 않아도 됨
- PEP 8 가이드(https://peps.python.org/pep-0008/)
- For simple public data attributes, it is best to expose just the attribute name, without complicated accessor(getter)/mutator(setter) methods. Keep in mind that Python provides an easy path to future enhancement, should you find that a simple data attribute needs to grow functional behavior. In that case, use properties to hide functional implementation behind simple data attribute access syntax.
- Note 1: Try to keep the functional behavior side-effect free, although side-effects such as caching are generally fine.(부작용이 발생하지 않도록 하고)
- Note 2: Avoid using properties for computationally expensive operations; the attribute notation makes the caller believe that access is (relatively) cheap.(계산이 비싼 작업은 피하자)
- For simple public data attributes, it is best to expose just the attribute name, without complicated accessor(getter)/mutator(setter) methods. Keep in mind that Python provides an easy path to future enhancement, should you find that a simple data attribute needs to grow functional behavior. In that case, use properties to hide functional implementation behind simple data attribute access syntax.
- Property 오용의 ChatGPT 조언
- 내부 상태를 관리를 위해서 사용하지 말자.
- 불필요한 API가 공개된다.
3. 어떻게 동작하는 가? 클래스 밖에서는 먼저 어떻게 동작하는 지 보고 클래스에 적용한 예시를 보자
def get_sex(self):
return self._sex
def set_sex(self, v):
print(f"change sex to {v}")
self._sex = v
sex_property = property(get_sex, doc="The sex of the person")
print(sex_property) # <property object at 0x100cad850>
sex_property = sex_property.setter(set_sex) # property 객체를 다시 반환한다.
class Person1:
sex = sex_property
def __init__(self, sex):
self.sex = sex
p1 = Person1("M") # change sex to M
print(p1.sex) # M
p1.sex = "F" # change sex to F
print(p1.sex) # F
p1._sex = "M" # set_sex 호출하지 않음, 또한 파이썬에서는 _로 시작하는 속성/메서드에는 클래스 외부에서 조회/접근 등을 하지 않는 암묵적인 룰이 있음
print(p1.sex) # M
class Person2:
def __init__(self, age):
# self._age = age # self._age에 age를 할당할때에는 setter(fset=set_age)을 호출하지 않는다.
self.age = age
def get_age(self):
return self._age
def set_age(self, v):
if v < 0:
raise ValueError('age cannot be lower than 0')
print(f"change age to {v}")
self._age = v
age = property(get_age, fset=set_age, doc="The age of the person")
p2 = Person2(10)
print(p2.age)
# p2.age 에 할당할때 set_age에서의 검증 에러로 ValueError 발생함
try:
p2.age = -10
except Exception as e:
print('error')
print(e) # age cannot be lower than 0
# p2._age 에 직접 할당하면서 set_age를 호출하지 않아 버그 생김
try:
p2._age = -10
print(p2.age) # -10
print('no error')
except Exception as e:
pass
class Person3:
def __init__(self, name):
self._name_called_count = 0
self.name = name # name으로 했을때 @name.setter을 호출한다.
@property # name = property(name) // 왼쪽 name은 property 객체를 할당한 변수, arg의 name은 함수
def name(self):
"""The name of the person"""
self._name_called_count += 1
print('called')
return self._name
# 위에서 name 메서드는 name = property(name)이다. 여기서 _name을 반환하는 함수
# name의 type은 property 이고 아래는 name = property(name, fset=name) 여기서 name은 아래의 _name을 변경하는 함수
@name.setter
def name(self, v): # name으로 함수명을 get부분처럼 통일해주어야 property의 객체의 setter로 인식한다.
print(f"change name to {v}")
self._name = v
p3 = Person3("lee") # change name to lee
print(p3.__dict__) # {'_name_called_count': 0, '_name': 'lee'}
p3.name = "kim" # change name to kim
print(p3.name) # called
print(p3.__dict__) # {'_name_called_count': 1, '_name': 'kim'}
# property 데코레이터를 통해 해당 변수는 property 타입이 되고,
# 변수.setter(name), property의 fset(age), property의 setter(sex) 를 통해 데코레이터를 통해 새로운 변수가 할당될때 호출하게 됨
4. 파이썬 내부에 구현된 property(https://github.com/python/cpython/blob/main/Objects/descrobject.c#L1507)
class property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
if doc is None and fget is not None and hasattr(fget, "__doc__"):
doc = fget.__doc__
self.__get = fget
self.__set = fset
self.__del = fdel
try:
self.__doc__ = doc
except AttributeError: # read-only or dict-less class
pass
def __get__(self, inst, type=None):
if inst is None:
return self
if self.__get is None:
raise AttributeError, "property has no getter"
return self.__get(inst)
def __set__(self, inst, value):
if self.__set is None:
raise AttributeError, "property has no setter"
return self.__set(inst, value)
def __delete__(self, inst):
if self.__del is None:
raise AttributeError, "property has no deleter"
return self.__del(inst)
# buitins.pyi
class property:
fget: Callable[[Any], Any] | None
fset: Callable[[Any, Any], None] | None
fdel: Callable[[Any], None] | None
__isabstractmethod__: bool
def __init__(
self,
fget: Callable[[Any], Any] | None = ...,
fset: Callable[[Any, Any], None] | None = ...,
fdel: Callable[[Any], None] | None = ...,
doc: str | None = ...,
) -> None: ...
def getter(self, __fget: Callable[[Any], Any]) -> property: ...
def setter(self, __fset: Callable[[Any, Any], None]) -> property: ...
def deleter(self, __fdel: Callable[[Any], None]) -> property: ...
def __get__(self, __obj: Any, __type: type | None = ...) -> Any: ...
def __set__(self, __obj: Any, __value: Any) -> None: ...
def __delete__(self, __obj: Any) -> None: ...
5. 요약
- 공개 API를 변경하지 않고 내부 구현을 변경 가능하고 동일한 메서드(property 데코레이터) 이름으로 가독성을 향상시키는 장점을 활용하자. 단, 계산이 비용 로직들을 넣는 것을 피하고 인스턴스 생성 후 속성 변경이 없는 케이스와 같은 단순한 클래스에서는 반드시 사용해야할 기능은 아니다.
- _를 포함한 변수와 아닌 변수에 대한 파이썬 컨벤션의 암묵적인 룰을 잘 숙지하자.
- Property는 데코레이터이고 클래스에 적용된 원리에 대해 이해하고 사용하자.
6. 참고한 링크들
-https://m.blog.naver.com/hankrah/221976126435
-https://www.freecodecamp.org/news/python-property-decorator/