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