모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

26강 / 전체 32강

리스트 심화 — 복사, 참조, 메모리 함정

8분 읽기 조회 4

얕은 복사와 깊은 복사의 차이를 완전히 이해하고 중첩 리스트 생성 함정과 방어적 복사 패턴을 배운다.

🎯 이 강의에서 배우는 것

파이썬에서 가장 많은 버그를 일으키는 주제 중 하나가 바로 복사와 참조입니다. 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

불러오는 중...