Data science/Python

python 매직메서드 (__repr__, __str__, __slots__)

연금(Pension)술사 2024. 6. 3. 19:35

 

객체표현: __repr__, __str__ 차이

파이썬에서 객체의 설명을 보려면, `repr()`, `str()`함수가 필요합니다. 이 두 함수는  `__repr__`와 `__str__`이 정의되어야 확인할 수 있습니다.  이 두 함수의 차이점은 아래와 같습니다.

공통점

  • 두 함수 모두 반환되는 형태가 `str` 입니다.

차이점

  • `repr`, `__repr__`은 주로 개발자을 위해 사용되며, 내부 디버깅을 위해 사용되는 표현방식입니다. 주로 `obj`을 직접 호출할 때 사용되고, 문자열 그 자체로 객체가 생성되도록 작성을 합니다.
  • `str`, `__str__`은 주로 사용자를 위해 사용되며, 보기쉬운 형태로 전환하거나 타입 캐스팅을 하는 용도로 사용됩니다. 또한, `__str__`이 정의되어있지 않다면, `__rerp__`이 호출되기도 합니다.
from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출 - __str__이 정의되지 않았으므로 __repr__이 대신 호출됩니다.
print(str(obj))  # MyClass(attribute1=10, attribute2='example')

 

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출 - __repr__이 정의되지 않았으므로 기본 구현이 호출됩니다.
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출
print(str(obj))  # MyClass with attribute1: 10 and attribute2: example

 

위의 코드에서 추가로 `MyClass(attribute1=10, attribute2='example')`을 그대로 호출해보면 객체가 생성이 가능합니다.

a = MyClass(attribute1=10, attribute2='example')
a
MyClass(attribute1=10, attribute2='example')

 

그 외에도, byte 시퀀스로 호출하려면 `byte()`함수를 호출하여 사용이 가능합니다. 이 때도, `__bytes__`매직메서드가 구현되어있어야합니다.

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

    def __bytes__(self):
        # 객체를 바이트 시퀀스로 변환
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2})".encode('utf-8')

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")
print(bytes(obj))  # b'MyClass(attribute1=10, attribute2=example)'
b'MyClass(attribute1=10, attribute2=example)'

 

객체 생성의 방법: @classmethod을 이용한 팩토리패턴

아래의 코드는 @classmethod에서 자기 자신의 클레스의 인스턴스를 생성 패턴입니다. 생성자를 꼭 만들필요없고, 대안적으로 아래와 같은 디자인 패턴이 가능합니다.

@dataclass
class MulticlassMetrics:
    accuracy: float
    auroc: float
    prauc: float

    @classmethod
    def calculate(
        cls, labels: np.ndarray, confidences: np.ndarray
    ) -> MulticlassMetrics:
        pred_label = np.argmax(confidences, axis=1)
        accuracy = (pred_label == labels).mean()
        auroc = calculate_auroc_ovr(confidences, labels)
        prauc = calculate_prauc_ovr(confidences, labels)

        return cls(accuracy, auroc, prauc)

    def to_dict(self, prefix=str()) -> dict:
        return {
            prefix + metric_name: round(value, 5)
            for metric_name, value in asdict(self).items()
        }

 

클레스 해시하기

파이썬 Set과 같은 기본자료구조에 넣으려면, 객체는 모두 hashable해야합니다. 즉, 해시값에 대해서 유일해야합니다. 이 내부적으로 hash()함수를 이용해서 객체를 해시하는데, 이를 이용하려면 `__hash__`을 정의해야합니다. 

이 디자인의 예시는 아래와 같습니다. HPO라는 온톨로지(개념)의 데이터클레스입니다. HPO의 id가 유일하므로 이 hash가 다르면 id가 내용이 각각 다름이 보장됩니다. 따라서, `__hash__`을 id을 기준으로 hash합니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

 

클레스 내 속성 보호하기: read only attributes : @property

파이썬에서는 비공개 속성(private attribute)을 생성하는 방법은 없지만, 수정을 막는 방법은 있습니다. @property을 이용하여 readonly로 작성할 수 있습니다.

아래의 예제 코드를 보면, all_subclasses을 `self._all_subclasses`로 저장해두고 @property로 읽어오려고합니다. 그리고, `_all_subclasses`을 `all_subclasses `을 통해서 볼 수는 있지만 직접 수정을 막기위해서, 감쌌습니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

    @property
    def all_subclasses(self) -> List[HPO]:
        """현재 클래스를 포함하여 모든 하위 클래스를 찾아 반환
        Return
            List[HPO]: 모든 하위 클래스의 ID 목록
        Example:
            >>> self.all_subclasses
            [
                HPO(...),
                HPO(...),
                ...
                HPO(...),
            ]
        """
        if hasattr(self, "_all_subclasses"):
            return self._all_subclasses

        container = set()
        self._get_subclasses_recursively(self, container)

        self._all_subclasses = [HPO(hpo_id) for hpo_id in container]
        return self._all_subclasses

 

__slot__ 으로 클레스 속성 메모리를 효율화 하기

파이썬에서는 객체의 속성이 생기면 `__dict__`라는 딕셔너리형 속성에 저장합니다. 따라서, 매우 빠르게 속성을 접근할 수 있습니다. 하지만, 반대로 속성이 매우 많은 객체라면, 메모리부담이 매우 커지게됩니다. 

아래와 같이 `__slots__`이라는 정의하면, 인스턴스에 `__dict__`라는 딕셔너리 속성이 저장되지않고, 고정된 튜플로만 관리합니다. 따라서, 동적으로 속성을 추가할 수는 없지만, dict객체를 따로 들고있지 않아도되니 속도가 빠르거나 메모리가 효율적으로 동작할 수 있습니다.

class Person:
    # __slots__를 사용하여 객체의 속성을 제한합니다.
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 객체 생성
person1 = Person('John', 30)

# 속성에 접근
print(person1.name)  # 출력: John
print(person1.age)   # 출력: 30

# 새로운 속성 추가 시 에러 발생
# person1.address = '123 Street'  # 'Person' object has no attribute 'address'

# __dict__를 사용하여 객체의 속성을 확인할 때 에러 발생
# print(person1.__dict__)  # AttributeError: 'Person' object has no attribute '__dict__'
반응형