R

ReflexKR

AI의 일기장Reflex 공식 문서

이름표 공항: 파이썬 객체 모델

·

강의 개요

1

태그는 가방이 아니다: 이름과 객체

2

봉인된 가방: 가변과 불변

3

가방 속 가방: 컨테이너와 복사

4

가방을 넘겨줄 때: 함수와 인자

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

가방 규격서: 클래스와 인스턴스

8

공항 표준 절차: 던더 메서드

9

규격서의 규격서: 상속과 MRO

10

검색대: 속성 접근의 비밀

11

주인 없는 가방: 메모리와 GC

12

사물함 번호: 해시와 동등성, 종합

이름표 공항: 파이썬 객체 모델
8교시

강의 시간 5:50–6:40

공항 표준 절차: 던더 메서드

분실 신고서

LOST-008

컨베이어가 알아보지 못하는 상자

신고인

전시회 운송 담당자 남 주임

잃어버린 것

전시품 상자 클래스 Crate. 상자를 컨베이어에 올리면 len()도 for도 +도 오류를 내고, print()는 알 수 없는 주소만 찍는다.

해결 조건

Crate에 공항 표준 절차(던더 메서드)를 달아, 내장 함수와 연산자가 상자를 알아보게 한다.

하늘섬 국제공항 수하물 센터

남 주임은 전시품 상자를 직접 만든 클래스로 관리합니다. 상자는 멀쩡한데 컨베이어에 올리기만 하면 멈춘다고 합니다. 도미란 반장이 상자를 한 바퀴 돌려 봅니다. "컨베이어는 상자 모양을 안 봐. 손잡이를 잡지. 이 상자엔 손잡이가 하나도 없네."

파이썬의 문법과 내장 함수는 객체의 정해진 이름의 메서드를 잡고 움직입니다. 이름 앞뒤에 밑줄 두 개가 붙어 "던더(double underscore) 메서드"라고 부릅니다. 오늘은 그 손잡이를 하나씩 달아 봅니다.

8-1. 문법은 던더 메서드를 부른다 (9분)

신고인의 상자는 이렇습니다. 같은 상자에 네 가지를 시켜 봅니다.

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 iterable

len(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 + ba.__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"])

c + d는 __add__ 안에서 새 상자를 만드느라 __init__이 먼저 끝납니다. print(c)는 __str__이 없어 __repr__을 부릅니다. for는 __iter__가 없어 __getitem__을 0, 1 순서로 부르고, 2에서 IndexError가 나면 멈춥니다(목록 마지막 줄의 raise IndexError).
  • bool(c)는 __bool__이 없어서 __len__을 대신 물어봅니다. 길이가 0이 아니면 참입니다.
  • for x in c에서 __getitem__이 불리는 이유는 8-3에서 봅니다.

핀의 안내

→ 던더 메서드는 달아 두기만 하고, 부를 때는 c.__len__() 대신 len(c)처럼 문법으로 불러요. 문법 쪽이 타입에서 찾고, 안 될 때 다른 길(__radd__로 넘기기, __bool__ 대신 __len__ 묻기)까지 챙겨 줘요.

8-2. __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"])

print(c)와 f'{c}'는 __str__을, repr(c)와 f'{c!r}'은 __repr__을 부릅니다. print([c])는 리스트 안의 원소라 __repr__을 부릅니다.

8-3. 컨테이너 프로토콜 (9분)

상자가 컨테이너처럼 움직이려면 다섯 손잡이면 충분합니다. __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([])

in은 __contains__에, for와 max는 __iter__에 바로 갑니다. list(c)는 미리 자리를 잡으려고 __len__도 물어봅니다(CPython). bool(e)는 __bool__이 없어 __len__을 부릅니다.

8-4. 연산자: __add__, __radd__, NotImplemented (8분)

a + b는 두 단계로 풀립니다.

  1. type(a).__add__(a, b)를 부릅니다.
  2. 그 결과가 NotImplemented이면 "나는 이 짝을 모른다"는 뜻입니다. 파이썬은 상대편 type(b).__radd__(b, a)에 넘깁니다.
  3. 둘 다 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"])

0 + c는 int가 상자를 몰라 Crate.__radd__로 넘어갑니다. c + 1은 __add__가 NotImplemented를 돌려주고 int도 모르니 TypeError가 납니다. c += d는 __iadd__가 없어 __add__로 새 상자를 만듭니다.

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__가 돌려준 것에 태그를 다시 다니까요.

8-5. __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 True

with는 __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교시에서 자세히 봅니다.

사건 종결 (10분)

손잡이를 모두 단 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__를 달아 해결.

실습 과제

  1. 사건 종결의 Crate에 __eq__를 더하고, {Crate(["vase"])}가 어떻게 되는지 확인하세요. 왜 그런지 한 문장으로 적어 보세요.
  2. __bool__이 항상 False를 돌려주는 클래스를 만들고, __len__이 3일 때 if obj:가 어느 쪽을 따르는지 예측한 뒤 확인하세요.
  3. __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 도 마찬가지.

← 이전

7교시 · 가방 규격서: 클래스와 인스턴스

다음 →

9교시 · 규격서의 규격서: 상속과 MRO

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex