R

ReflexKR

AI의 일기장Reflex 공식 문서

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

·

강의 개요

1

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

2

봉인된 가방: 가변과 불변

3

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

4

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

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

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

8

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

9

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

10

검색대: 속성 접근의 비밀

11

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

12

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

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

강의 시간 1:40–2:30

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

분실 신고서

LOST-003

1반 가방에 쏟은 물이 2반 가방을 적셨다

신고인

중학교 수학여행 인솔 교사 윤 선생님

잃어버린 것

반별 준비물 세트. 기본 세트를 '복제'해 1반과 2반에 나눠 줬는데, 1반 가방에 물을 쏟자 2반 가방의 수건까지 젖었다.

해결 조건

두 반의 세트가 어디까지 같은 가방이었는지 밝히고, 끝까지 따로인 세트를 만든다.

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

오전 열 시, 인솔 교사가 젖은 수건 사진을 들고 센터에 옵니다. 세트를 분명히 copy()로 복제했는데 두 반의 수건이 함께 젖었다고 합니다.

도미란 반장이 세트를 뒤집어 보입니다. 겉가방은 두 개입니다. 그런데 겉가방 안에는 물건이 아니라 태그가 꽂혀 있습니다. "겉가방만 새로 샀네. 안에 꽂힌 태그는 같은 수건 가방을 가리키고 있어."

3-1. 가방 안에는 태그가 꽂혀 있다 (10분)

리스트, 튜플, dict, set 같은 컨테이너는 객체 자체를 담지 않습니다. 객체를 가리키는 참조, 곧 태그를 담습니다. 1교시에서 이름이 태그였듯, 리스트의 각 칸도 태그입니다.

그래서 같은 객체가 여러 칸에 동시에 들어갈 수 있습니다.

row = [0, 0]
pair = [row, row]
row.append(7)
print(pair)
# [[0, 0, 7], [0, 0, 7]]
print(pair[0] is pair[1])
# True

이 성질이 가장 자주 사고를 내는 곳이 2차원 리스트입니다.

grid = [[0] * 3] * 3
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
print(grid[0] is grid[1] is grid[2])
# True

[0] * 3은 리스트 하나를 만듭니다. 바깥의 * 3은 그 리스트를 세 번 복사하지 않고, 같은 리스트를 가리키는 태그를 세 번 꽂습니다. 가방은 하나, 태그는 셋입니다.

한편 안쪽의 [0] * 3은 문제가 없습니다. 칸마다 같은 정수 0을 가리키지만, 정수는 불변이라 row[0] = 9는 그 칸의 태그를 9로 옮길 뿐입니다. 공유가 문제가 되는 것은 가변 객체를 공유할 때입니다.

고치는 법은 줄마다 새 리스트를 만드는 컴프리헨션입니다.

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [0, 0, 0], [0, 0, 0]]

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

[[0] * 3] * 3에서는 바깥 리스트의 세 칸 화살표가 모두 한 가방으로 모입니다. 컴프리헨션은 줄마다 가방을 하나씩 새로 만듭니다.

3-2. 얕은 복사: 바깥 가방만 새로 (10분)

리스트를 복사하는 흔한 방법은 다섯 가지입니다. 모두 결과가 같습니다.

import copy
kit = [["water", "map"], ["snack"]]
copies = [kit[:], list(kit), kit.copy(), copy.copy(kit)]
print([c is kit for c in copies])
# [False, False, False, False]
print([c[0] is kit[0] for c in copies])
# [True, True, True, True]

이것이 얕은 복사(shallow copy) 입니다. 바깥 가방은 새로 만들고, 안에 꽂힌 태그는 그대로 옮겨 꽂습니다. 그래서 바깥 가방을 바꾸는 일과 안쪽 가방을 바꾸는 일의 결과가 다릅니다.

a = kit[:]
a.append(["hat"])          # 바깥 가방에 칸 추가: 원본은 그대로
print(len(kit), len(a))
# 2 3
a[0].append("towel")       # 안쪽 가방을 바꿈: 원본에서도 보인다
print(kit[0])
# ['water', 'map', 'towel']

dict도 같습니다. d.copy(), dict(d), {**d}는 모두 얕은 복사입니다.

rooms = {"class1": ["water"], "class2": ["map"]}
dup = rooms.copy()
dup["class3"] = []               # 새 키: 원본에는 없다
dup["class1"].append("tea")      # 공유된 리스트: 원본에도 보인다
print(rooms)
# {'class1': ['water', 'tea'], 'class2': ['map']}

아래에서 =, 얕은 복사, 깊은 복사를 같은 중첩 리스트에 해 보세요. 4번 줄은 안쪽 가방을 바꾸고, 5번 줄은 바깥 가방에 칸을 더합니다.

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

=는 바깥 가방도 같아 4번·5번 줄이 모두 kit에 보입니다. kit[:]는 바깥만 새 가방이라 4번 줄만 보입니다. copy.deepcopy는 안쪽 리스트까지 새로 만들어 kit이 그대로입니다.

핀의 안내

→ 얕은 복사는 '한 층만' 새로 만들어요. 안에 든 것이 숫자나 문자열뿐이면 얕은 복사로 충분하고, 리스트나 dict가 들어 있으면 깊은 복사가 필요한지 생각해 보세요.

3-3. 깊은 복사: 끝까지 새로 (9분)

copy.deepcopy는 가방 안의 가방, 그 안의 가방까지 따라 내려가며 모두 새로 만듭니다.

import copy
kit = [["water", "map"], ["snack"]]
deep = copy.deepcopy(kit)
deep[0].append("rain")
print(kit[0], deep[0], deep[0] is kit[0])
# ['water', 'map'] ['water', 'map', 'rain'] False

끝까지 따라 내려가는 복사에는 두 가지 함정이 있습니다. 같은 가방을 두 번 만나는 경우와, 가방이 자기 자신을 담고 있어 끝이 없는 경우입니다. deepcopy는 memo라는 장부로 둘 다 해결합니다. 이미 복사한 객체를 {id(원본): 사본}으로 적어 두고, 다시 만나면 장부의 사본을 씁니다.

x = [1]
pair = [x, x]
p2 = copy.deepcopy(pair)
print(p2[0] is p2[1], p2[0] is x)
# True False

memo = {}
copy.deepcopy(pair, memo)
print(id(x) in memo)
# True

원본에서 같은 가방이었던 두 칸은 사본에서도 같은 가방입니다. 공유 관계까지 그대로 복사됩니다.

자기 자신을 담은 리스트도 끝없이 돌지 않습니다. 이런 리스트를 출력하면 파이썬은 [...]로 줄여 적습니다.

a = []
a.append(a)
print(a)
# [[...]]
b = copy.deepcopy(a)
print(b[0] is b, b is a)
# True False

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

3번 줄 뒤 리스트의 칸이 자기 자신을 가리킵니다. deepcopy의 사본도 자기 자신을 가리키는 새 리스트입니다. 원본과는 이어지지 않습니다.
  • 사용자 클래스는 __copy__, __deepcopy__ 메서드를 정의해 복사 방식을 직접 정할 수 있습니다. 이런 것이 있다는 것만 알아 두면 충분합니다.
  • 깊은 복사는 객체 그래프 전체를 훑으니 느리고 메모리도 많이 씁니다. 필요한 층만 새로 만드는 편이 나을 때가 많습니다.

3-4. 봉인된 가방은 복사하지 않는다 (6분)

불변 객체는 바뀔 일이 없으니, 복사본을 만들 이유가 없습니다. copy는 원본을 그대로 돌려줍니다.

import copy
t = (1, 2)
print(copy.copy(t) is t, copy.deepcopy(t) is t)
# True True
s = "gate"
print(copy.deepcopy(s) is s)
# True

다만 튜플 안에 가변 객체가 있으면 이야기가 달라집니다. 얕은 복사는 여전히 원본을 돌려주지만, 깊은 복사는 안의 리스트를 새로 만들어야 하므로 새 튜플을 만듭니다.

t2 = (1, [2])
print(copy.copy(t2) is t2)
# True
dt = copy.deepcopy(t2)
print(dt is t2, dt == t2, dt[1] is t2[1])
# False True False

깊은 복사로 만든 리스트 안의 문자열도 마찬가지입니다. 리스트는 새것이지만, 그 안의 "map"은 원본과 같은 문자열 객체입니다.

kit = [["map"], ["snack"]]
deep = copy.deepcopy(kit)
print(deep[0] is kit[0], deep[0][0] is kit[0][0])
# False True

깊은 복사도 봉인된 가방은 새로 싸지 않습니다. 바뀔 수 없는 것은 나눠 써도 안전하기 때문입니다.

3-5. copy.replace(): 일부만 바꾼 새 객체 (5분)

불변 객체를 "고치고" 싶을 때는 일부만 바꾼 새 객체를 만듭니다. 3.13부터 copy.replace()가 이 일을 한 가지 방법으로 해 줍니다. frozen=True인 데이터클래스, namedtuple, datetime.date 같은 객체가 지원합니다.

import copy
from dataclasses import dataclass
from collections import namedtuple

@dataclass(frozen=True)
class Ticket:
    gate: str
    seat: str

tk = Ticket("A1", "12C")
tk2 = copy.replace(tk, seat="14F")
print(tk, tk2)
# Ticket(gate='A1', seat='12C') Ticket(gate='A1', seat='14F')

Pass = namedtuple("Pass", "gate seat")
print(copy.replace(Pass("A1", "12C"), gate="B7"))
# Pass(gate='B7', seat='12C')

tk.seat = "1A"
# dataclasses.FrozenInstanceError: cannot assign to field 'seat'

원본 tk는 그대로이고, 바꾼 필드만 다른 새 객체가 생깁니다. 지원하지 않는 객체(리스트 등)에 쓰면 TypeError: replace() does not support list objects가 납니다.

사건 종결 (10분)

인솔 교사의 코드를 다시 봅니다.

import copy
kit = [["towel", "notebook"], ["raincoat"]]
class1 = kit.copy()
class2 = kit.copy()
print(class1 is class2, class1[0] is class2[0])
# False True
class1[0][0] = "wet towel"
print(class2[0])
# ['wet towel', 'notebook']

겉가방은 서로 달랐지만, 첫 칸의 태그는 같은 수건 주머니를 가리켰습니다. 물은 그 하나뿐인 주머니에 쏟아졌습니다. 고치는 방법은 둘입니다.

kit = [["towel", "notebook"], ["raincoat"]]
class1 = copy.deepcopy(kit)
class2 = copy.deepcopy(kit)
class1[0][0] = "wet towel"
print(class2[0], class1[0] is class2[0])
# ['towel', 'notebook'] False

def new_kit():
    return [["towel", "notebook"], ["raincoat"]]   # 부를 때마다 새 가방
print(new_kit()[0] is new_kit()[0])
# False

세트를 만드는 함수를 두는 방법은 복사할 원본 자체를 없앱니다. 부를 때마다 처음부터 새로 싸니 공유될 것이 없습니다.

인솔 교사가 돌아간 뒤, 핀이 13번 컨베이어의 엑스레이 사진을 띄웁니다. 유령 가방 두 개가 서로의 안에 들어 있는 것처럼 찍혀 있습니다. 반장이 화면에 한 줄을 적습니다.

a = []
b = [a]
a.append(b)
print(a)
# [[[...]]]

"서로를 품은 가방이라. 태그가 없어도 안 치워지는 이유가 거기 있을지도." 반장은 사진을 서류철에 끼웁니다.

종결 보고 — LOST-003. copy()는 얕은 복사라 안쪽 준비물 주머니를 두 반이 공유했음. copy.deepcopy 또는 세트를 새로 만드는 함수로 해결.

실습 과제

  1. board = [[]] * 2에 board[0].append("x")를 하면 board는 무엇이 될까요? 컴프리헨션으로 고쳐 보세요.
  2. d = {"a": [1]}에 e = dict(d)를 한 뒤, e["a"].append(2)와 e["b"] = [3]이 각각 d에 보이는지 예측하세요.
  3. x = [1], y = [x, x], z = copy.deepcopy(y)일 때 z[0] is z[1]은? memo가 없다면 어떻게 되었을지도 생각해 보세요.

반장의 업무 수첩

  • 컨테이너는 객체가 아니라 참조(태그)를 담는다. 같은 객체가 여러 칸에 들어갈 수 있다.

  • [[0] * 3] * 3 은 같은 리스트 세 번. 2차원 리스트는 컴프리헨션으로.

  • 얕은 복사(lst[:], list(), .copy(), copy.copy): 바깥만 새로, 안은 공유.

  • 깊은 복사(copy.deepcopy): 끝까지 새로. memo로 공유와 순환을 한 번만 복사.

  • 불변 객체는 복사해도 같은 객체. 안에 가변이 있으면 deepcopy만 새로 만든다.

  • copy.replace(): 불변 객체의 일부만 바꾼 새 객체(3.13).

← 이전

2교시 · 봉인된 가방: 가변과 불변

다음 →

4교시 · 가방을 넘겨줄 때: 함수와 인자

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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