목차
15 장

매직 메소드 깊이와 프로토콜

파이썬 객체가 언어 기능과 통합되는 모든 후크. __call__, __getitem__, __hash__, __format__, __getattr__ 등을 한곳에 정리합니다.

3부 깊이 · 동시성의 첫 챕터입니다. 2부를 거쳐 @dataclass, 데코레이터, 컨텍스트 매니저 같은 “도구를 쓰는 사람"의 어휘가 잡혔다면, 3부부터는 그 도구들이 어떻게 만들어졌는지의 시점으로 들어갑니다.

본 챕터의 매직 메소드는 책 전체에서 가장 자주 만나게 됩니다. 8장 dataclass가 자동으로 만들어주던 __init__ / __repr__ / __eq__, 9장 Protocol, 10장 컨텍스트 매니저__enter__ / __exit__, 11장 이터러블, 제너레이터__iter__ / __next__ — 전부 매직 메소드입니다. 본 챕터는 그 후크들을 한곳에 모아 정리합니다.

매직 메소드(또는 던더 메소드, dunder = double underscore)는 파이썬 객체가 언어 기능과 만나는 공식 후크입니다. len(x)x.__len__()를 부르고, a + ba.__add__(b)를 부르는 식입니다. 본 후크를 정확히 알면 파이썬다운 객체를 만들 수 있고, 라이브러리 코드를 읽을 때 무엇이 호출되는지 보입니다.

객체 생명 주기 #

__init____new__ — 차이 #

둘의 역할
class Foo:
    def __new__(cls, *args, **kwargs):
        # 인스턴스를 만든다 (메모리 할당)
        instance = super().__new__(cls)
        return instance

    def __init__(self, value):
        # 만들어진 인스턴스를 초기화한다
        self.value = value
  • __new__ — 클래스 메소드처럼 동작, 인스턴스를 만들어 반환. cls를 첫 인자로 받음
  • __init__ — 인스턴스 메소드, 이미 만들어진 인스턴스를 초기화. self를 받음

대부분의 코드는 __init__만 적습니다. __new__가 필요한 경우는 좁습니다:

  • 불변 타입(tuple, str)을 상속해 만들기
  • 싱글턴 같은 인스턴스 캐싱
  • __init__ 호출 자체를 막아야 할 때
__new__ 가 다른 객체를 반환하면
class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)   # True

__new__가 반환한 객체가 그 클래스의 인스턴스가 아니면 __init__이 호출되지 않습니다. 미묘한 부분이니 주의.

__del__ — 거의 안 씀 #

객체가 가비지 컬렉션될 때 호출됩니다. 다만:

  • 언제 호출될지 보장되지 않음 — GC 타이밍에 의존
  • 순환 참조가 있으면 호출 안 될 수도 있음
  • 예외가 나도 무시됨

자원 정리는 __del__이 아니라 컨텍스트 매니저 (10장)를 쓰는 편이 안전합니다.

표현 — __repr__ vs __str__ #

repr / str
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self) -> str:
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"

p = Point(1, 2)
print(repr(p))   # Point(x=1, y=2)   ← 디버깅용, 모호하지 않게
print(str(p))    # (1, 2)            ← 사람이 읽기 좋게
print(f"{p}")    # (1, 2)            ← f-string 은 str
print(p)         # (1, 2)            ← print 도 str

룰:

  • __repr__모호하지 않은 표현. 가능하면 eval(repr(x)) == x가 되도록
  • __str__사용자에게 보여줄 표현. 정의 안 하면 __repr__가 사용됨

@dataclass (8장)가 자동으로 만드는 건 __repr__입니다.

__format__ — f-string의 포맷 스펙 #

포맷 스펙 받기
class Money:
    def __init__(self, amount, currency):
        self.amount, self.currency = amount, currency

    def __format__(self, spec):
        if spec == "k":
            return f"{self.amount / 1000:.1f}k {self.currency}"
        return f"{self.amount} {self.currency}"

m = Money(12345, "KRW")
print(f"{m}")     # 12345 KRW
print(f"{m:k}")   # 12.3k KRW

f-string f"{x:fmt}"fmt 부분이 __format__(spec)의 인자로 들어갑니다. 도메인 객체에 사용자 정의 포맷을 부여하는 후크입니다.

비교 #

__eq____hash__ — 짝꿍 #

이 둘은 항상 같이 갑니다.

기본 룰
class User:
    def __init__(self, id, name):
        self.id, self.name = id, name

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

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

룰:

  • a == bhash(a) == hash(b)반드시 성립해야 한다
  • __eq__만 정의하면 __hash__가 자동으로 None이 되어 set / dict 키로 못 씀
  • 가변 객체는 보통 hashable이 아님

@dataclass(frozen=True)가 두 메소드를 같이 자동 생성합니다.

__lt__ 등 비교 — functools.total_ordering #

<, <=, >, >=, ==, != 여섯을 다 적기 귀찮을 때.

total_ordering
from functools import total_ordering

@total_ordering
class Score:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __lt__(self, other):
        return self.value < other.value
# 나머지 4개는 자동으로 채워짐

@dataclass(order=True)가 더 짧지만 단순 필드 비교에만 어울립니다. 복잡한 비교 로직은 total_ordering으로.

컨테이너처럼 — 시퀀스 / 매핑 #

__len__, __getitem__, __contains__, __iter__ #

시퀀스 만들기
class Page:
    def __init__(self, items):
        self.items = items

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

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

    def __contains__(self, value):
        return value in self.items

    def __iter__(self):
        return iter(self.items)

p = Page(["a", "b", "c"])
len(p)            # 3
p[0]              # 'a'
"b" in p          # True
list(p)           # ['a', 'b', 'c']
for x in p: ...   # OK

이 네 개를 채우면 거의 list 처럼 동작합니다. 사실 __getitem____len__만 있어도 for in이 동작합니다 (인덱스 0부터 IndexError까지 시도).

슬라이스도 자동 #

__getitem__의 인자에 slice 객체가 들어올 수 있습니다.

슬라이스 처리
class MyList:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, key):
        if isinstance(key, slice):
            return MyList(self.items[key])
        return self.items[key]

m = MyList([1, 2, 3, 4, 5])
m[1:3].items   # [2, 3]

m[1:3] 호출 시 keyslice(1, 3, None)입니다.

__setitem__, __delitem__ #

쓰기 / 삭제
class Cache:
    def __init__(self):
        self._data = {}

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

c = Cache()
c["x"] = 1
print(c["x"])   # 1
del c["x"]

dict처럼 동작하는 객체를 만들 때 사용합니다.

호출 가능 — __call__ #

객체를 함수처럼 호출할 수 있게 만드는 후크.

__call__
class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self, value):
        self.count += 1
        return value * 2

c = Counter()
print(c(5))        # 10
print(c(7))        # 14
print(c.count)     # 2

12장 데코레이터 패턴클래스 형태 데코레이터가 본 후크 위에 만들어졌습니다. PyTorch의 nn.Module__call__을 가져 모델을 함수처럼 호출하게 합니다.

속성 접근 — __getattr__, __setattr__, __getattribute__ #

__getattr__ — 없는 속성 요청 시 #

없는 속성 lazy 처리
class Lazy:
    def __getattr__(self, name):
        if name.startswith("get_"):
            field = name[4:]
            return lambda: f"value of {field}"
        raise AttributeError(name)

l = Lazy()
l.get_name()   # 'value of name'
l.get_age()    # 'value of age'

없는 속성에만 호출됩니다. 있는 속성은 정상 경로를 탑니다. ORM의 자동 메소드, 프록시 객체 등에 자주 등장.

__getattribute__ — 모든 속성 요청에 #

모든 속성 가로채기 — 위험
class All:
    def __getattribute__(self, name):
        print(f"접근: {name}")
        return super().__getattribute__(name)

모든 속성 접근에 끼어듭니다. 잘못 쓰면 무한 재귀가 나기 쉬워서 (자기 안에서 self.x를 쓰면 또 __getattribute__ 호출), 보통은 __getattr__만 씁니다.

__setattr__, __delattr__ #

쓰기 가로채기
class Frozen:
    def __init__(self, x):
        self.x = x
        object.__setattr__(self, "_locked", True)

    def __setattr__(self, name, value):
        if getattr(self, "_locked", False):
            raise AttributeError(f"{name} 변경 불가")
        super().__setattr__(name, value)

f = Frozen(5)
f.x = 10   # AttributeError

@dataclass(frozen=True)의 동작이 정확히 본 패턴입니다.

산술 연산 — __add__#

연산자 오버로딩
class Vec:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        return Vec(self.x + other.x, self.y + other.y)

    def __mul__(self, k):
        return Vec(self.x * k, self.y * k)

    def __rmul__(self, k):
        return self.__mul__(k)

v = Vec(1, 2) + Vec(3, 4)        # __add__
w = Vec(1, 2) * 3                 # __mul__
u = 3 * Vec(1, 2)                 # __rmul__ (왼쪽 피연산자가 모르는 타입)

대칭 연산이 필요하면 __rmul__ 같은 reflected 버전도 정의합니다. 왼쪽 피연산자가 자기를 곱할 줄 모를 때 (예: int * Vec) 호출됩니다.

__bool__ — 진리값 #

bool
class Bag:
    def __init__(self):
        self.items = []

    def __bool__(self):
        return bool(self.items)

b = Bag()
if b:
    print("뭔가 있음")

정의 안 하면 __len__을 보고, 그것도 없으면 항상 True. 컨테이너 형태에는 __len__만 있어도 충분한 경우가 많습니다.

상속 시점의 후크 — __init_subclass__ #

서브클래스가 만들어질 때 호출되는 후크. 메타클래스를 안 쓰고도 비슷한 일을 할 수 있습니다.

__init_subclass__
class Plugin:
    registry = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry.append(cls)

class JsonPlugin(Plugin):
    pass

class CsvPlugin(Plugin):
    pass

print(Plugin.registry)
# [<class 'JsonPlugin'>, <class 'CsvPlugin'>]

플러그인 자동 등록 같은 경우에 자주 씁니다. 메타클래스 (17장 메타클래스)가 더 강력하지만, 이 정도 일은 __init_subclass__가 더 가볍고 안전합니다.

자주 만나는 메소드 — 한 표 #

카테고리메소드호출 시점
생성 / 소멸__new__, __init__, __del__인스턴스 만들 때 / 정리될 때
표현__repr__, __str__, __format__, __bool__repr(), str(), f"", if
비교__eq__, __hash__, __lt__==, hash(), <
컨테이너__len__, __getitem__, __setitem__, __contains__, __iter__len(), x[k], in, for
호출__call__x(...)
속성__getattr__, __setattr__, __delattr__속성 접근
산술__add__, __sub__, __mul__, __truediv__, …+, -, *, /
비동기__await__, __aiter__, __anext__, __aenter__, __aexit__await, async for / with
상속 후크__init_subclass__, __class_getitem__서브클래스 / Cls[T]

본 표를 다 외울 필요는 없습니다. 어떤 후크가 있다는 것만 인지하면 라이브러리 코드에서 보일 때 검색할 수 있습니다.

연습문제 #

  1. Money 클래스를 만드세요. 필드는 amount: int, currency: str. __repr__Money(amount=1234, currency='KRW'), __str__1,234 KRW 형식. __format__으로 f"{m:k}"1.2k KRW처럼 천 단위로 표시되게 만듭니다.
  2. Page 클래스를 만드세요. __len__ / __getitem__ (슬라이스 처리 포함) / __contains__ / __iter__를 구현해 list처럼 동작하게 합니다. Page([1,2,3,4,5])[1:3]가 새 Page([2,3])를 반환하는지 확인.
  3. __init_subclass__로 자동 플러그인 등록기를 만드세요. class Plugin:의 서브클래스가 만들어지면 클래스 변수 registry: list[type]에 자동으로 추가됩니다. 두 서브클래스를 정의한 뒤 Plugin.registry를 출력해 확인합니다.

한 줄 요약: 매직 메소드는 객체가 언어 기능과 만나는 공식 후크. __new__ (생성) vs __init__ (초기화), __repr__ (개발자) vs __str__ (사용자), __eq____hash__는 짝꿍, 컨테이너는 __len__ + __getitem__, 함수처럼은 __call__, 속성은 __getattr__ (없는 것만) / __getattribute__ (모든 것, 위험), 서브클래스 자동 등록은 __init_subclass__.

다음 챕터 #

다음 16장 디스크립터와 __set_name__에서는 매직 메소드 중에서도 특수한 분류 — 속성을 객체화하는 디스크립터를 다룹니다. @property가 사실 디스크립터의 한 형태입니다.

X