-
Python - PropertyPython 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/
'Python' 카테고리의 다른 글
Asyncio - async, await (0) 2022.07.02 Flask - Celery - 비동기 처리하기 (0) 2022.02.07 Python Asyncio - 코루틴, 크롤링 (0) 2022.01.18 OOP - 상속,변수, 초기화- Crawling/Custom Exceptions (0) 2022.01.13 [TEST] 2.테스트 코드 작성하기 (0) 2021.10.24 - Property