얕은 복사와 깊은 복사의 차이를 완전히 이해하고 중첩 리스트 생성 함정과 방어적 복사 패턴을 배운다.
🎯 이 강의에서 배우는 것
파이썬에서 가장 많은 버그를 일으키는 주제 중 하나가 바로 복사와 참조입니다. b = a는 복사가 아닙니다. [[0]*3]*3으로 만든 2차원 리스트는 한 행을 수정하면 모든 행이 바뀝니다. 이 강의에서 얕은 복사와 깊은 복사를 완전히 이해합니다.
🔗 할당은 복사가 아니다 — 이름표만 추가
a = [1, 2, 3]
b = a # b는 a를 "복사"한 것이 아님! 같은 객체를 가리키는 이름표 추가
print(id(a) == id(b)) # True — 같은 객체!
print(a is b) # True
b.append(4)
print(a) # [1, 2, 3, 4] ← a도 변경됨!
b[0] = 99
print(a) # [99, 2, 3, 4] ← a도 변경됨!
# 함수 인자도 마찬가지
def add_item(lst, item):
lst.append(item) # 원본 수정!
my_list = [1, 2, 3]
add_item(my_list, 4)
print(my_list) # [1, 2, 3, 4] ← 원본이 변경됨
# 의도하지 않은 수정을 막으려면 명시적으로 복사
def add_item_safe(lst, item):
new_lst = lst.copy() # 방어적 복사
new_lst.append(item)
return new_lst
my_list = [1, 2, 3]
result = add_item_safe(my_list, 4)
print(my_list) # [1, 2, 3] ← 원본 보존
print(result) # [1, 2, 3, 4]
🔵 얕은 복사 (Shallow Copy)
import copy
original = [1, 2, 3]
# 얕은 복사 3가지 방법 (모두 동일)
copy1 = original.copy()
copy2 = list(original)
copy3 = original[:]
# 모두 새로운 리스트 객체
print(original is copy1) # False ← 다른 객체
print(id(original) != id(copy1)) # True
# 1차원 리스트: 완전한 복사처럼 동작
copy1[0] = 99
print(original) # [1, 2, 3] ← 변경 없음 ✅
# 문제: 중첩(2차원) 리스트
nested = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
shallow = nested.copy()
print(nested is shallow) # False ← 다른 리스트
print(nested[0] is shallow[0]) # True ← 같은 내부 리스트!
# 내부 리스트 수정하면 두 리스트 모두 영향
shallow[0][0] = 99
print(nested) # [[99, 2, 3], [4, 5, 6], [7, 8, 9]] ← 영향받음!
print(shallow) # [[99, 2, 3], [4, 5, 6], [7, 8, 9]]
# 시각화:
# nested ────→ [ ref0, ref1, ref2 ]
# ↓ ↓ ↓
# [1,2,3] [4,5,6] [7,8,9]
# ↑
# shallow ──→ [ ref0, ref1, ref2 ]
# ← 내부 리스트(ref0, ref1, ref2)는 공유됨!
🔴 깊은 복사 (Deep Copy)
import copy
nested = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
deep = copy.deepcopy(nested) # 모든 수준을 재귀적으로 복사
print(nested is deep) # False
print(nested[0] is deep[0]) # False ← 내부 리스트도 다른 객체!
deep[0][0] = 99
print(nested) # [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ← 변경 없음! ✅
print(deep) # [[99, 2, 3], [4, 5, 6], [7, 8, 9]]
# deepcopy는 순환 참조도 처리
circular = [1, 2]
circular.append(circular) # 자기 자신을 포함!
# print(circular) # [1, 2, [...]] ← 무한 재귀적 표현
deep_circular = copy.deepcopy(circular) # 오류 없이 처리
print(deep_circular[0]) # 1
print(deep_circular[2] is deep_circular) # True ← 순환 구조 유지
# 언제 무엇을 쓰는가:
# b = a → 같은 객체 (의도적 공유)
# a.copy() → 최상위만 복사 (1차원 리스트 또는 내부 불변)
# copy.deepcopy → 모든 수준 복사 (중첩 가변 객체)
# 비용: b=a (O(1)) < a.copy() (O(n)) < deepcopy (O(m), m은 전체 객체 수)
⚠️ 중첩 리스트 생성 함정
# 3x3 행렬 만들기
# 잘못된 방법: * 연산자로 내부 리스트 복제
matrix_wrong = [[0] * 3] * 3
print(matrix_wrong) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
matrix_wrong[0][0] = 1
print(matrix_wrong) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ← 세 행 모두 변경!
# [0,0,0] 리스트 하나를 3번 참조하기 때문
# 올바른 방법: 컴프리헨션으로 각각 새 리스트 생성
matrix_right = [[0] * 3 for _ in range(3)]
print(matrix_right) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
matrix_right[0][0] = 1
print(matrix_right) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] ← 첫 행만 변경! ✅
# 확인: 내부 리스트가 다른 객체인지 확인
print(matrix_right[0] is matrix_right[1]) # False ← 다른 객체
print(matrix_wrong[0] is matrix_wrong[1]) # True ← 같은 객체!
# 일반 패턴:
# 1차원 리스트 * n 은 OK: [0] * 5 → [0, 0, 0, 0, 0] (int는 불변이니까)
# 2차원 리스트 * n 은 위험: [[0]] * 3 → 같은 내부 리스트 3개
💾 메모리 효율 — 불필요한 복사 피하기
import sys
# 대용량 리스트 처리 시 불필요한 복사 주의
big = list(range(1_000_000))
# 나쁜 예: 불필요한 복사
def sum_bad(lst):
copy = lst[:] # 100만 개 복사 — 불필요!
return sum(copy)
# 좋은 예: 읽기만 할 때는 복사 불필요
def sum_good(lst):
return sum(lst) # 원본 읽기만, 복사 없음
# 이터러블 소비 vs 리스트 생성
nums = range(1_000_000) # lazy — 메모리 거의 없음
total_gen = sum(x for x in nums) # O(1) 메모리
total_list = sum(list(x for x in nums)) # O(n) 메모리 — 리스트 생성 불필요!
# 방어적 복사 패턴 — 원본 보호가 필요할 때만
def process_data(data: list) -> list:
working_copy = data.copy() # 수정할 것이라면 복사
working_copy.sort()
return working_copy
original = [3, 1, 4, 1, 5]
result = process_data(original)
print(original) # [3, 1, 4, 1, 5] ← 보존
print(result) # [1, 1, 3, 4, 5]
⚠️ 자주 하는 실수
b = a를 복사로 착각: 이름 바인딩입니다. 같은 객체를 가리킵니다. b를 수정하면 a도 변경됩니다. 복사가 필요하면b = a.copy()를 명시하세요.[[0]*n]*m으로 2차원 리스트 생성: 한 행을 수정하면 모든 행이 바뀝니다. 반드시[[0]*n for _ in range(m)]을 사용하세요.- 깊은 복사가 필요한데 얕은 복사 사용: 중첩된 가변 객체(리스트 안의 리스트, 딕셔너리 안의 리스트)는 얕은 복사만으로 완전히 독립되지 않습니다.
copy.deepcopy()를 사용하세요.
📝 정리 및 다음 강의 예고
b = a: 이름 추가, 같은 객체 공유. 수정하면 둘 다 영향.- 얕은 복사(
a.copy()): 최상위 리스트는 새로 생성, 내부 객체는 공유. 1차원은 OK, 중첩이면 내부 변경 시 원본 영향. - 깊은 복사(
copy.deepcopy()): 모든 수준 재귀 복사. 비용이 크므로 필요할 때만. [[0]*n]*m함정: 내부 리스트 공유.[[0]*n for _ in range(m)]로 해결.
다음 강의: 튜플(tuple) — 불변성이 왜 가치 있는가. 언제 리스트 대신 튜플을 써야 하는지, 네임드튜플로 가독성을 높이는 방법을 배웁니다.
관련 주제
- 얕은 복사(Shallow Copy)
- 깊은 복사(deepcopy)
- 중첩 리스트 생성 함정
- 리스트 참조 공유
- 방어적 복사 패턴
- 함수 인자 가변객체 전달
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
