Skip to main content

매직 메서드 (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)진리값
__getattribute__는 모든 속성 접근에서 호출되고, __getattr__는 일반적인 방법으로 속성을 찾지 못했을 때만 호출됩니다. 일반적으로 __getattr__을 사용합니다.
__eq__를 정의하면 __hash__가 자동으로 None이 되어 해시 불가능해집니다. 딕셔너리 키나 집합 요소로 사용하려면 __hash__도 구현해야 합니다.

체크리스트

  • __str____repr__의 차이를 설명할 수 있다
  • __getitem____len__으로 시퀀스 프로토콜을 구현할 수 있다
  • __call__로 호출 가능 객체를 만들 수 있다
  • __enter__/__exit__으로 컨텍스트 매니저를 구현할 수 있다

다음 문서