분실 신고서
LOST-003
신고인
중학교 수학여행 인솔 교사 윤 선생님
잃어버린 것
반별 준비물 세트. 기본 세트를 '복제'해 1반과 2반에 나눠 줬는데, 1반 가방에 물을 쏟자 2반 가방의 수건까지 젖었다.
해결 조건
두 반의 세트가 어디까지 같은 가방이었는지 밝히고, 끝까지 따로인 세트를 만든다.
하늘섬 국제공항 수하물 센터
오전 열 시, 인솔 교사가 젖은 수건 사진을 들고 센터에 옵니다. 세트를 분명히 copy()로 복제했는데 두 반의 수건이 함께 젖었다고 합니다.
도미란 반장이 세트를 뒤집어 보입니다. 겉가방은 두 개입니다. 그런데 겉가방 안에는 물건이 아니라 태그가 꽂혀 있습니다. "겉가방만 새로 샀네. 안에 꽂힌 태그는 같은 수건 가방을 가리키고 있어."
리스트, 튜플, 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
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
리스트를 복사하는 흔한 방법은 다섯 가지입니다. 모두 결과가 같습니다.
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
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
핀의 안내
→ 얕은 복사는 '한 층만' 새로 만들어요. 안에 든 것이 숫자나 문자열뿐이면 얕은 복사로 충분하고, 리스트나 dict가 들어 있으면 깊은 복사가 필요한지 생각해 보세요.
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
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
__copy__, __deepcopy__ 메서드를 정의해 복사 방식을 직접 정할 수 있습니다. 이런 것이 있다는 것만 알아 두면 충분합니다.불변 객체는 바뀔 일이 없으니, 복사본을 만들 이유가 없습니다. 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깊은 복사도 봉인된 가방은 새로 싸지 않습니다. 바뀔 수 없는 것은 나눠 써도 안전하기 때문입니다.
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가 납니다.
인솔 교사의 코드를 다시 봅니다.
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 또는 세트를 새로 만드는 함수로 해결.
board = [[]] * 2에 board[0].append("x")를 하면 board는 무엇이 될까요? 컴프리헨션으로 고쳐 보세요.d = {"a": [1]}에 e = dict(d)를 한 뒤, e["a"].append(2)와 e["b"] = [3]이 각각 d에 보이는지 예측하세요.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).
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.