분실 신고서
LOST-008
신고인
전시회 운송 담당자 남 주임
잃어버린 것
전시품 상자 클래스 Crate. 상자를 컨베이어에 올리면 len()도 for도 +도 오류를 내고, print()는 알 수 없는 주소만 찍는다.
해결 조건
Crate에 공항 표준 절차(던더 메서드)를 달아, 내장 함수와 연산자가 상자를 알아보게 한다.
하늘섬 국제공항 수하물 센터
남 주임은 전시품 상자를 직접 만든 클래스로 관리합니다. 상자는 멀쩡한데 컨베이어에 올리기만 하면 멈춘다고 합니다. 도미란 반장이 상자를 한 바퀴 돌려 봅니다. "컨베이어는 상자 모양을 안 봐. 손잡이를 잡지. 이 상자엔 손잡이가 하나도 없네."
파이썬의 문법과 내장 함수는 객체의 정해진 이름의 메서드를 잡고 움직입니다. 이름 앞뒤에 밑줄 두 개가 붙어 "던더(double underscore) 메서드"라고 부릅니다. 오늘은 그 손잡이를 하나씩 달아 봅니다.
신고인의 상자는 이렇습니다. 같은 상자에 네 가지를 시켜 봅니다.
class Crate:
def __init__(self, items):
self.items = list(items)
c = Crate(["vase", "lamp"])
for expr in ["len(c)", "c[0]", "c + c", "iter(c)"]:
try:
eval(expr)
except TypeError as e:
print(f"{expr:7} → {e}")
# len(c) → object of type 'Crate' has no len()
# c[0] → 'Crate' object is not subscriptable
# c + c → unsupported operand type(s) for +: 'Crate' and 'Crate'
# iter(c) → 'Crate' object is not iterablelen(c)는 마법이 아닙니다. 파이썬은 type(c).__len__(c)를 부릅니다. 그 메서드가 없으니 "len이 없다"는 오류가 납니다. __len__을 달면 바로 됩니다.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
c = Crate(["vase", "lamp"])
print(len(c), type(c).__len__(c))
# 2 2
c.__len__ = lambda: 99
print(len(c), c.__len__())
# 2 99마지막 두 줄이 중요합니다. 인스턴스에 __len__을 붙여도 len(c)는 2입니다. 문법이 부르는 던더 메서드는 인스턴스가 아니라 타입(클래스)에서 찾기 때문입니다. 규격서에 적힌 절차만 컨베이어가 따릅니다.
| 문법 | 실제로 불리는 것 |
|---|---|
len(x) | type(x).__len__(x) |
x[i], x[i] = v | __getitem__, __setitem__ |
a + b | a.__add__(b), 안 되면 b.__radd__(a) |
a == b | __eq__ |
v in x | __contains__ (없으면 반복하며 비교) |
for v in x | __iter__ (없으면 __getitem__) |
bool(x), if x: | __bool__ (없으면 __len__) |
print(x), str(x) | __str__ (없으면 __repr__) |
x() | __call__ |
with x: | __enter__, __exit__ |
이런 손잡이를 잘 단 클래스를 흔히 "파이썬다운 클래스"라고 합니다. 따로 size()나 merge() 메서드를 외울 필요 없이, 이미 아는 len과 +로 다룰 수 있기 때문입니다.
아래 교환기는 손잡이를 단 Crate에 문법 한 줄을 넣고, 파이썬이 실제로 부른 메서드를 순서대로 보여 줍니다.
던더 교환기
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __len__(self):
7
return len(self.items)
8
def __getitem__(self, i):
9
return self.items[i]
10
def __add__(self, other):
11
return Crate(self.items + other.items)
12
13
c = Crate(["vase", "lamp"])
14
d = Crate(["cup"])
bool(c)는 __bool__이 없어서 __len__을 대신 물어봅니다. 길이가 0이 아니면 참입니다.for x in c에서 __getitem__이 불리는 이유는 8-3에서 봅니다.핀의 안내
→ 던더 메서드는 달아 두기만 하고, 부를 때는 c.__len__() 대신 len(c)처럼 문법으로 불러요. 문법 쪽이 타입에서 찾고, 안 될 때 다른 길(__radd__로 넘기기, __bool__ 대신 __len__ 묻기)까지 챙겨 줘요.
__repr__과 __str__ (7분)신고인이 디버깅하려고 상자를 찍으면 이런 글자만 나왔습니다.
class Crate:
def __init__(self, items):
self.items = list(items)
print(Crate(["vase"]))
# 예: <__main__.Crate object at 0x000001F4C2A8B2C0>object가 물려준 기본 __repr__은 타입 이름과 주소뿐입니다. 상자 안에 무엇이 들었는지 알 수 없습니다. 글자로 바꾸는 손잡이는 두 개입니다.
| 누구를 위한 글자 | 쓰이는 곳 | |
|---|---|---|
__repr__ | 개발자. 가능하면 Crate([...])처럼 다시 만들 수 있는 모양 | repr(), 대화형 셸, 디버거, 리스트·dict 안의 원소, f-string의 !r |
__str__ | 사용자. 읽기 좋은 문장 | print(), str(), f-string의 {x} |
class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __str__(self):
return f"crate of {len(self.items)}"
c = Crate(["vase", "lamp"])
print(c)
# crate of 2
print(repr(c))
# Crate(['vase', 'lamp'])
print([c])
# [Crate(['vase', 'lamp'])]
print(f"{c} / {c!r}")
# crate of 2 / Crate(['vase', 'lamp'])print([c])에서는 __str__이 아니라 __repr__이 쓰였습니다. 리스트를 글자로 만들 때 원소는 언제나 repr로 적습니다.__str__이 없으면 str()과 print()는 __repr__을 씁니다. 반대는 아닙니다. 그래서 하나만 단다면 __repr__부터 답니다.repr입니다. 버그를 쫓을 때 보이는 모습이 곧 __repr__입니다.던더 교환기
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __str__(self):
7
return f"crate of {len(self.items)}"
8
9
c = Crate(["vase", "lamp"])
상자가 컨테이너처럼 움직이려면 다섯 손잡이면 충분합니다. __len__, __getitem__, __iter__, __contains__, __bool__입니다. 그런데 8-1에서는 __iter__가 없는데도 for가 돌았습니다.
class OldCrate:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
print("getitem", i)
return self.items[i]
for x in OldCrate(["vase", "lamp"]):
pass
# getitem 0
# getitem 1
# getitem 2
print("lamp" in OldCrate(["vase", "lamp"]))
# getitem 0
# getitem 1
# True__iter__가 없으면 iter()는 __getitem__을 0, 1, 2… 순서로 부르는 반복자를 만듭니다. IndexError가 나면 "끝"으로 알고 조용히 멈춥니다. 그래서 getitem 2까지 찍히고 오류 없이 끝났습니다. __contains__가 없으면 in은 이렇게 하나씩 꺼내 ==로 비교합니다. 찾으면 거기서 멈춥니다.
이 길은 오래된 호환용입니다. 새로 만드는 클래스에는 손잡이를 제대로 답니다.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __iter__(self):
yield from self.items
def __contains__(self, x):
return x in self.items
c, e = Crate(["vase", "lamp"]), Crate([])
print(list(c), "lamp" in c, bool(c), bool(e))
# ['vase', 'lamp'] True True False
if not e:
print("empty crate")
# empty crate__iter__는 반복자를 돌려줘야 합니다. return iter(self.items)도 되고, 위처럼 yield를 쓰면 함수가 제너레이터가 되어 반복자를 저절로 만듭니다. 제너레이터는 제너레이터·데코레이터 강의에서 더 깊이 다룹니다.__contains__가 있으면 in은 반복하지 않고 그 메서드에 바로 묻습니다.__bool__이 없으면 __len__이 0인지로 참거짓을 정합니다. 둘 다 없으면 객체는 언제나 참입니다. 그래서 빈 Crate도 __len__만 달면 if not e:가 자연스럽게 됩니다.던더 교환기
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __len__(self):
5
return len(self.items)
6
def __iter__(self):
7
yield from self.items
8
def __contains__(self, x):
9
return x in self.items
10
11
c = Crate(["vase", "lamp"])
12
e = Crate([])
__add__, __radd__, NotImplemented (8분)a + b는 두 단계로 풀립니다.
type(a).__add__(a, b)를 부릅니다.NotImplemented이면 "나는 이 짝을 모른다"는 뜻입니다. 파이썬은 상대편 type(b).__radd__(b, a)에 넘깁니다.NotImplemented이면 TypeError입니다.class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
c, d = Crate(["vase"]), Crate(["cup"])
print(c + d)
# Crate(['vase', 'cup'])
print(sum([c, d]))
# Crate(['vase', 'cup'])
c + 1
# TypeError: unsupported operand type(s) for +: 'Crate' and 'int'sum()은 0에서 시작해 0 + c부터 계산합니다. int.__add__(0, c)는 상자를 몰라 NotImplemented를 돌려주고, 그래서 Crate.__radd__(c, 0)이 불립니다. __radd__가 0을 받아 주니 sum이 됩니다.NotImplemented를 돌려줍니다. 그래야 상대편에게 기회가 갑니다. NotImplementedError(예외)와는 다른 것입니다.던더 교환기
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __add__(self, other):
7
if not isinstance(other, Crate):
8
return NotImplemented
9
return Crate(self.items + other.items)
10
def __radd__(self, other):
11
if other == 0:
12
return self
13
return NotImplemented
14
15
c = Crate(["vase"])
16
d = Crate(["cup"])
2교시에서 본 +=의 두 얼굴이 여기서 풀립니다. a += b는 먼저 __iadd__(제자리 더하기)를 찾고, 없으면 a = a + b로 바꿔 새 객체에 태그를 옮깁니다. list에는 __iadd__가 있고 tuple과 str에는 없습니다.
c = Crate(["vase"])
alias = c
c += Crate(["cup"])
print(c is alias, alias)
# False Crate(['vase'])
class Crate2(Crate):
def __iadd__(self, other):
self.items += other.items
return self
c = Crate2(["vase"])
alias = c
c += Crate2(["cup"])
print(c is alias, alias)
# True Crate(['vase', 'cup'])__iadd__가 있으면 가방 안을 바꾸고 자기 자신을 돌려줍니다. 파이썬은 그 반환값에 c 태그를 다시 매다는데, 같은 가방이니 태그는 그대로입니다. (class Crate2(Crate)는 Crate를 물려받는 규격서입니다. 9교시의 주제입니다.)
핀의 안내
→ __iadd__에서 return self를 빠뜨리면 c += d 뒤에 c가 None이 돼요. 파이썬은 __iadd__가 돌려준 것에 태그를 다시 다니까요.
__call__, with, @dataclass (7분)__call__을 달면 객체를 함수처럼 부를 수 있습니다. 설정값을 들고 다니는 함수가 필요할 때 씁니다(6교시의 클로저와 같은 일을 클래스로).
class Scale:
def __init__(self, limit):
self.limit = limit
def __call__(self, kg):
return kg <= self.limit
check = Scale(23)
print(check(20), check(30), callable(check))
# True False Truewith는 __enter__와 __exit__을 부릅니다. 블록 안에서 예외가 나도 __exit__은 반드시 불립니다. 자원 정리에 쓰는 이유이고, 11교시에서 다시 만납니다.
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, exc_type, exc, tb):
print("belt off", exc_type)
return False
with Belt() as b:
print("moving")
# belt on
# moving
# belt off None__exit__이 True를 돌려주면 예외를 삼킵니다. 대부분은 False(또는 아무것도 돌려주지 않기)가 맞습니다.
__init__, __repr__, __eq__처럼 늘 비슷하게 쓰는 손잡이는 @dataclass가 만들어 줍니다.
from dataclasses import dataclass
@dataclass
class Tag:
code: str
kg: int = 0
t = Tag("ICN", 12)
print(t, t == Tag("ICN", 12))
# Tag(code='ICN', kg=12) True
print(Tag.__hash__)
# None마지막 줄을 보세요. __eq__를 정의하면 파이썬은 __hash__를 None으로 만듭니다. 직접 쓴 클래스도 같습니다.
class Crate:
def __init__(self, items):
self.items = list(items)
def __eq__(self, other):
return isinstance(other, Crate) and self.items == other.items
print(Crate(["vase"]) == Crate(["vase"]), Crate.__hash__)
# True None
{Crate(["vase"])}
# TypeError: cannot use 'Crate' as a set element (unhashable type: 'Crate')"값이 같으면 같다"고 정했는데 해시는 여전히 정체성(주소) 기준이면, 같은 두 상자가 set의 다른 칸에 들어갑니다. 그 약속을 깨지 않으려고 해시를 아예 꺼 둔 것입니다. 해시와 동등성의 약속은 12교시에서 자세히 봅니다.
손잡이를 모두 단 Crate입니다.
class Crate:
def __init__(self, items=()):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
def __iter__(self):
return iter(self.items)
def __contains__(self, x):
return x in self.items
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
return self if other == 0 else NotImplemented
a, b = Crate(["vase", "lamp"]), Crate(["frame"])
print(len(a), a[0], "lamp" in a, [x.upper() for x in a])
# 2 vase True ['VASE', 'LAMP']
print(a + b, sum([a, b]))
# Crate(['vase', 'lamp', 'frame']) Crate(['vase', 'lamp', 'frame'])
print(a[-1], a[:1])
# lamp ['vase']a[:1]은 __getitem__에 슬라이스 객체가 넘어가 리스트의 슬라이스가 그대로 돌아왔습니다. 상자로 돌려주고 싶다면 isinstance(i, slice)로 나눠 처리하면 됩니다.
남 주임의 상자가 컨베이어 위를 조용히 지나갑니다. 반장이 말합니다. "모양은 그대로야. 손잡이만 달았지."
종결 보고 — LOST-008. Crate에 손잡이(던더 메서드)가 없어 문법이 상자를 알아보지 못했음. repr·len·getitem·iter·contains·add·__radd__를 달아 해결.
Crate에 __eq__를 더하고, {Crate(["vase"])}가 어떻게 되는지 확인하세요. 왜 그런지 한 문장으로 적어 보세요.__bool__이 항상 False를 돌려주는 클래스를 만들고, __len__이 3일 때 if obj:가 어느 쪽을 따르는지 예측한 뒤 확인하세요.__getitem__이 isinstance(i, slice)이면 Crate를 돌려주도록 고쳐, a[:1]이 Crate(['vase'])가 되게 하세요.반장의 업무 수첩
문법은 던더 메서드를 부른다. len(x) 는 type(x).__len__(x). 인스턴스가 아니라 타입에서 찾는다.
하나만 단다면 __repr__. print 는 __str__ 이 없으면 __repr__ 을 쓰고, 리스트 속 원소는 늘 repr.
for 는 __iter__, 없으면 __getitem__ 을 0부터(IndexError에서 멈춤). bool 은 __bool__, 없으면 __len__.
모르는 짝에는 NotImplemented 를 돌려준다. 그러면 상대편 __radd__ 에 기회가 간다.
a += b 는 __iadd__ 가 있으면 제자리(return self), 없으면 a = a + b.
__eq__ 를 정의하면 __hash__ 는 None. @dataclass 도 마찬가지.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.