ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Python - Property
    Python 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.(계산이 비싼 작업은 피하자)
    • 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/

     

Designed by Tistory.