python 매직메서드 (__repr__, __str__, __slots__)
객체표현: __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__'