매직 메서드 (Magic Methods)
학습 목표
- 매직 메서드(던더 메서드)의 역할을 이해한다
- 문자열 표현, 비교, 산술 연산을 커스터마이징할 수 있다
__getitem__, __len__으로 시퀀스 프로토콜을 구현할 수 있다
__call__, __enter__/__exit__을 활용할 수 있다
왜 중요한가
매직 메서드(Magic Method, Dunder Method)는 Python의 내장 연산자와 함수가 호출하는 특수 메서드입니다. len(obj)는 obj.__len__()을, obj[i]는 obj.__getitem__(i)을 호출합니다. PyTorch의 Dataset.__getitem__(), nn.Module.__call__()이 모두 매직 메서드입니다.
문자열 표현
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""사용자 친화적 표현 (print, str에서 사용)"""
return f"({self.x}, {self.y})"
def __repr__(self):
"""개발자용 표현 (디버깅, REPL에서 사용)"""
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(p) # (3, 4) <- __str__
print(repr(p)) # Point(x=3, y=4) <- __repr__
print(f"좌표: {p}") # 좌표: (3, 4) <- __str__
__repr__은 가능하면 eval(repr(obj))로 원본 객체를 재생성할 수 있는 형태로 작성합니다. __str__이 없으면 __repr__이 대신 사용됩니다.
비교 연산
from functools import total_ordering
@total_ordering # __eq__와 __lt__만 정의하면 나머지 자동 생성
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __eq__(self, other):
return self.score == other.score
def __lt__(self, other):
return self.score < other.score
def __repr__(self):
return f"Student({self.name!r}, {self.score})"
students = [
Student("김철수", 85),
Student("이영희", 92),
Student("박민수", 78),
]
print(sorted(students))
# [Student('박민수', 78), Student('김철수', 85), Student('이영희', 92)]
print(max(students)) # Student('이영희', 92)
산술 연산
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""오른쪽 곱셈: scalar * vector"""
return self.__mul__(scalar)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 3) # Vector(9, 12)
print(2 * v1) # Vector(6, 8) <- __rmul__
print(abs(v1)) # 5.0
시퀀스 프로토콜: getitem, len
class Dataset:
"""간단한 데이터셋 클래스 (PyTorch Dataset 패턴)"""
def __init__(self, data, labels):
self.data = data
self.labels = labels
def __len__(self):
"""len(dataset) 지원"""
return len(self.data)
def __getitem__(self, idx):
"""dataset[idx] 지원 - 슬라이싱도 가능"""
if isinstance(idx, slice):
return [(self.data[i], self.labels[i])
for i in range(*idx.indices(len(self)))]
return self.data[idx], self.labels[idx]
def __contains__(self, item):
"""item in dataset 지원"""
return item in self.data
def __iter__(self):
"""for item in dataset 지원"""
for i in range(len(self)):
yield self[i]
# 사용
ds = Dataset([10, 20, 30, 40, 50], ["a", "b", "c", "d", "e"])
print(len(ds)) # 5
print(ds[0]) # (10, "a")
print(ds[1:3]) # [(20, "b"), (30, "c")]
print(30 in ds) # True
for data, label in ds:
print(f"{data} -> {label}")
call - 호출 가능 객체
class Predictor:
"""호출 가능한 예측기"""
def __init__(self, threshold=0.5):
self.threshold = threshold
def __call__(self, probabilities):
"""인스턴스를 함수처럼 호출"""
return [1 if p >= self.threshold else 0 for p in probabilities]
predict = Predictor(threshold=0.5)
probs = [0.8, 0.3, 0.6, 0.1, 0.9]
labels = predict(probs) # 인스턴스를 함수처럼 호출!
print(labels) # [1, 0, 1, 0, 1]
print(callable(predict)) # True
컨텍스트 매니저: enter, exit
class Timer:
"""실행 시간을 측정하는 컨텍스트 매니저"""
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"실행 시간: {self.elapsed:.4f}초")
return False # 예외를 전파
# with 문과 함께 사용
with Timer():
total = sum(range(1_000_000))
# 실행 시간: 0.0234초
주요 매직 메서드 요약
| 매직 메서드 | 호출 방법 | 용도 |
|---|
__str__ | str(obj), print(obj) | 사용자 표현 |
__repr__ | repr(obj) | 개발자 표현 |
__len__ | len(obj) | 길이 |
__getitem__ | obj[key] | 인덱스 접근 |
__setitem__ | obj[key] = val | 인덱스 설정 |
__contains__ | item in obj | 멤버십 테스트 |
__call__ | obj() | 호출 가능 |
__iter__ | for x in obj | 반복 |
__eq__ | obj == other | 동등 비교 |
__hash__ | hash(obj) | 해시 값 |
__bool__ | bool(obj) | 진리값 |
__getattr__과 __getattribute__의 차이는?
__getattribute__는 모든 속성 접근에서 호출되고, __getattr__는 일반적인 방법으로 속성을 찾지 못했을 때만 호출됩니다. 일반적으로 __getattr__을 사용합니다.
__eq__를 정의하면 __hash__가 자동으로 None이 되어 해시 불가능해집니다. 딕셔너리 키나 집합 요소로 사용하려면 __hash__도 구현해야 합니다.
체크리스트
다음 문서