파이썬 매직 메서드 완벽 가이드: 커스텀 연산자와 객체 동작 제어

파이썬 매직 메서드의 원리부터 __repr__, __add__, 비교·해시, 반복, 인덱싱, 컨텍스트 관리자 구현까지 예제로 정리합니다.

본문 상단 광고 구역 (승인 후 자동 노출됩니다)
연산과 비교 및 반복 장치로 객체 동작을 표현한 파이썬 매직 메서드 이미지
파이썬 던더 메서드로 커스텀 연산자와 객체 동작을 구현하는 방법

파이썬 매직 메서드, 즉 던더 메서드는 __init__, __repr__, __len__, __add__처럼 이름 앞뒤에 이중 밑줄이 붙은 특별한 메서드입니다. 직접 만든 클래스가 +, len(), 비교, 반복, 인덱싱 같은 파이썬 문법에 어떻게 반응할지 정의할 때 사용하며, 임의로 호출하기보다 해당 문법을 통해 파이썬이 호출하도록 설계하는 것이 핵심입니다.

Python 데이터 모델에서 특수 메서드 확인하기

핵심 요약

  • 던더 메서드는 클래스 객체를 파이썬의 내장 문법과 연결하는 프로토콜입니다.
  • __repr__은 개발자용 표현, __str__은 사용자용 표현을 담당합니다.
  • __eq____hash__는 딕셔너리 키·집합 사용까지 영향을 주므로 함께 설계해야 합니다.
  • 지원하지 않는 이항 연산은 무리하게 예외를 만들기보다 NotImplemented를 반환해야 역방향 연산 기회가 생깁니다.
  • 내장형의 기대 동작을 따르고, 놀라운 부작용을 넣지 않는 것이 좋은 구현입니다.

1. 매직 메서드는 언제 호출되나요?

던더 메서드는 연산자와 내장 함수가 객체에 요청을 보내는 약속입니다. 예를 들어 len(obj)는 객체 유형의 __len__ 구현을 이용하고, obj1 + obj2는 덧셈 관련 특수 메서드를 확인합니다. 코드에서 obj.__len__()처럼 직접 호출할 수도 있지만 일반적으로는 len(obj)를 사용해야 파이썬 데이터 모델의 일관성을 유지할 수 있습니다.

파이썬 문법 연결되는 대표 메서드 용도
str(obj) __str__ 읽기 쉬운 문자열
repr(obj) __repr__ 개발·디버깅 표현
len(obj) __len__ 항목 개수
obj[key] __getitem__ 인덱스·키 접근
left + right __add__ 덧셈 의미 정의
for x in obj __iter__ 반복 지원

모든 메서드를 구현할 필요는 없습니다. 객체가 어떤 개념을 나타내고 어떤 문법이 자연스러운지 먼저 정한 뒤 필요한 프로토콜만 선택하는 편이 좋습니다.

2. 객체 생성과 문자열 표현

__init__은 이미 생성된 인스턴스의 초기 상태를 설정합니다. 객체를 실제로 만드는 과정에는 __new__도 관여하지만, 불변형을 상속하는 등 특별한 이유가 없다면 보통 __init__부터 구현합니다.

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product(name={self.name!r}, price={self.price!r})"

    def __str__(self):
        return f"{self.name}: {self.price:,}원"

__repr__은 가능하다면 객체를 명확히 식별하고 재생성에 도움이 되는 표현을 반환하는 것이 좋습니다. __str__은 최종 사용자에게 보여줄 읽기 쉬운 표현입니다. __str__이 없으면 파이썬은 __repr__을 대신 사용할 수 있습니다. 로그에 객체를 남길 때 표현 메서드가 유용하지만, 비밀번호나 개인정보가 노출되지 않도록 주의해야 합니다.

3. __add__로 커스텀 연산자 구현하기

연산자 오버로딩은 기호의 우선순위를 바꾸는 기능이 아니라, 사용자 정의 객체에 해당 연산의 의미를 제공하는 기능입니다. 금액, 좌표, 벡터처럼 덧셈 의미가 명확한 객체에 적용하면 코드가 읽기 쉬워집니다.

from dataclasses import dataclass

@dataclass(frozen=True)
class Vector:
    x: float
    y: float

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(x=4, y=6)

호환되지 않는 형식이면 NotImplemented를 반환해야 파이썬이 상대 객체의 __radd__ 같은 역방향 연산을 시도할 수 있습니다. 실제로 지원하지 않는 연산이라면 최종적으로 파이썬이 적절한 TypeError를 발생시킵니다. None을 반환하면 실패가 늦게 드러나므로 피해야 합니다.

자주 쓰는 산술 메서드

  • __add__, __sub__, __mul__: 일반 이항 연산
  • __radd__, __rsub__: 왼쪽 피연산자가 처리하지 못한 역방향 연산
  • __iadd__, __imul__: +=, *= 같은 제자리 연산

4. 객체 비교와 __hash__ 설계

__eq__는 두 객체가 값의 관점에서 같은지 정의합니다. 정렬 비교에는 __lt__, __le__, __gt__, __ge__가 사용됩니다. 모든 비교 메서드를 직접 만들기 전에 객체에 자연스러운 전체 순서가 있는지 판단해야 합니다.

class User:
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.user_id == other.user_id

    def __hash__(self):
        return hash(self.user_id)

같다고 비교되는 객체는 같은 해시값을 가져야 합니다. 또한 딕셔너리 키나 집합 원소로 사용되는 동안 해시에 참여하는 값이 바뀌면 안 됩니다. 따라서 가변 객체에 __hash__를 무리하게 제공하지 않는 편이 안전합니다. __eq__만 재정의하면 해시 가능 여부가 달라질 수 있으므로 딕셔너리와 집합 사용까지 테스트해야 합니다.

5. 내 클래스를 컨테이너처럼 동작시키기

__len__, __getitem__, __contains__, __iter__를 목적에 맞게 구현하면 객체가 파이썬 컨테이너 문법과 자연스럽게 연동됩니다.

class Playlist:
    def __init__(self, songs):
        self._songs = list(songs)

    def __len__(self):
        return len(self._songs)

    def __getitem__(self, index):
        return self._songs[index]

    def __iter__(self):
        yield from self._songs

playlist = Playlist(["봄", "여름", "가을"])
print(len(playlist))
print(playlist[0])
for song in playlist:
    print(song)

__len__은 0 이상의 정수를 반환해야 합니다. __getitem__에서 슬라이스도 지원하려면 전달된 키가 slice인지 고려해야 합니다. 반복을 직접 구현할 때 __iter__에서 제너레이터를 사용하면 별도 이터레이터 클래스를 만들지 않고도 현재 값을 하나씩 전달할 수 있습니다.

6. with 문과 호출 가능한 객체 만들기

__enter____exit__를 구현하면 객체를 with 문에서 사용할 수 있습니다. __exit__는 예외 형식, 예외 값, 트레이스백을 전달받으며, 참인 값을 반환하면 예외를 억제하므로 특별한 복구 의도가 없다면 함부로 True를 반환하지 않아야 합니다.

class Timer:
    def __enter__(self):
        from time import perf_counter
        self.started = perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        from time import perf_counter
        self.elapsed = perf_counter() - self.started
        print(f"{self.elapsed:.4f}초")
        return False

with Timer():
    total = sum(range(100_000))

__call__을 구현하면 인스턴스를 함수처럼 호출할 수도 있습니다. 설정과 상태를 유지하는 변환기나 검증기에는 유용하지만, 단순 함수로 충분하다면 클래스를 추가하지 않는 편이 더 명료할 수 있습니다.

7. 목적별 매직 메서드 선택표

원하는 객체 동작 우선 검토할 메서드 확인할 점
사람이 읽는 출력 __str__ 민감정보 노출 여부
디버깅 표현 __repr__ 객체를 명확히 식별하는지
값 비교 __eq__ 다른 형식에 NotImplemented
딕셔너리 키 __eq__, __hash__ 동등성·불변성 일치
반복 가능한 객체 __iter__ 반복 때마다 새 이터레이터인지
인덱스 접근 __getitem__ 범위 오류와 슬라이스
자원 관리 __enter__, __exit__ 예외 처리와 해제 보장

8. 실수 방지 체크리스트

  • 의미가 불분명한 연산자를 편의만으로 추가하지 않았는지 확인합니다.
  • 지원하지 않는 이항 연산에서 NotImplemented를 반환하는지 확인합니다.
  • __repr__와 로그에 개인정보·비밀값이 노출되지 않는지 점검합니다.
  • __eq__를 바꿨다면 __hash__와 집합 동작도 확인합니다.
  • __len__이 음수나 정수가 아닌 값을 반환하지 않는지 확인합니다.
  • __iter__가 자기 자신을 잘못 재사용해 두 번째 반복을 비우지 않는지 테스트합니다.
  • __exit__에서 의도하지 않게 예외를 억제하지 않는지 확인합니다.
  • 파이썬이 이미 사용하는 던더 이름을 임의의 일반 메서드 용도로 만들지 않습니다.

자주 묻는 질문

1. 던더 메서드와 매직 메서드는 같은 뜻인가요?

대체로 같은 대상을 가리킵니다. 던더는 이름 앞뒤의 double underscore를 줄여 부르는 표현입니다.

2. 매직 메서드를 직접 호출해도 되나요?

가능하지만 일반적으로 obj.__len__()보다 len(obj)처럼 대응하는 문법이나 내장 함수를 사용하는 것이 좋습니다.

3. __init__이 객체를 생성하나요?

__new__가 인스턴스 생성에 관여하고 __init__은 생성된 인스턴스를 초기화합니다.

4. __repr__과 __str__을 모두 구현해야 하나요?

필수는 아닙니다. 개발 중 객체를 명확히 확인하려면 우선 __repr__을 구현하고, 별도의 사용자용 표현이 필요할 때 __str__을 추가할 수 있습니다.

5. __add__에서 형식이 다르면 왜 NotImplemented를 반환하나요?

파이썬이 상대 피연산자의 역방향 메서드를 시도하고, 모두 지원하지 않을 때 일관된 오류를 만들 수 있게 하기 위해서입니다.

6. __eq__만 구현하면 집합 원소로 쓸 수 있나요?

항상 그렇지는 않습니다. 해시 가능 여부와 해시값의 불변성도 충족해야 하므로 __hash__ 설계를 함께 확인해야 합니다.

7. for 문을 지원하려면 무엇을 구현해야 하나요?

일반적으로 __iter__가 이터레이터를 반환하도록 구현합니다. 제너레이터 함수를 사용하면 간결하게 만들 수 있습니다.

8. __getitem__만 있어도 반복할 수 있나요?

일부 시퀀스 방식에서는 0부터 차례로 접근해 반복될 수 있지만, 의도를 분명히 하려면 __iter__를 명시하는 것이 좋습니다.

9. __del__로 파일을 닫아도 되나요?

객체가 정확히 언제 정리될지 기대하기 어렵습니다. 파일 같은 자원은 with 문과 컨텍스트 관리자를 우선 사용해야 합니다.

10. 모든 연산자를 오버로딩하는 것이 좋은가요?

아닙니다. 객체 개념과 자연스럽게 맞는 연산만 구현해야 사용자가 결과를 예측할 수 있습니다.

공식 출처

공식 문서 확인일: 2026년 9월 2일

본문 하단 광고 구역