
리스트를 복사한 뒤 사본만 수정했는데 원본까지 바뀌는 경우가 있다. b = a는 같은 객체에 이름을 하나 더 붙이는 대입이고, a.copy()는 바깥 리스트만 새로 만드는 얕은 복사이기 때문이다.
중첩 리스트에서는 바깥 리스트와 내부 원소의 관계를 나눠 보면 동작이 명확해진다. 내부 리스트까지 분리하려면 deepcopy()를 검토하되, 모든 객체를 항상 새로 만든다고 생각하면 다른 오해가 생긴다. 아래 예제는 Python 3.12.14에서 실행했다.
대입은 복사가 아니다
original = [10, 20]
alias = original
alias.append(30)
print(original) # [10, 20, 30]
print(alias is original) # True
original과 alias는 같은 리스트를 가리킨다. 어떤 이름을 통해 append()를 호출해도 같은 객체가 바뀐다. 여기서 is는 두 값의 내용이 같은지보다 동일한 객체인지 확인하는 연산자다.
얕은 복사는 바깥 리스트를 새로 만든다
original = [[1, 2], [3, 4]]
copied = original.copy()
print(copied is original) # False
print(copied[0] is original[0]) # True
copied.append([5, 6])
copied[0].append(9)
print(original) # [[1, 2, 9], [3, 4]]
print(copied) # [[1, 2, 9], [3, 4], [5, 6]]
copied.append([5, 6])는 새로 만든 바깥 리스트만 수정하므로 원본의 길이는 늘어나지 않는다. 반면 copied[0].append(9)는 두 리스트가 공유하는 내부 리스트를 수정한다. 그래서 양쪽 첫 번째 원소에 모두 9가 보인다.
기본 list의 copy(), 전체 슬라이스 a[:], list(a)는 이런 얕은 복사를 만든다. 원소가 정수처럼 변경할 수 없는 객체라면 얕은 복사만으로 원하는 독립성을 얻는 경우도 많다. 내부에 리스트나 딕셔너리처럼 변경 가능한 객체가 있는지가 판단의 기준이다.
원소 교체와 내부 객체 수정은 다르다
original = [[1, 2], [3, 4]]
copied = original.copy()
copied[0] = [99]
print(original) # [[1, 2], [3, 4]]
print(copied) # [[99], [3, 4]]
이번에는 공유한 내부 리스트를 수정하지 않았다. 사본의 첫 번째 칸이 다른 리스트를 가리키도록 바꿨다. 바깥 리스트는 이미 별도 객체이므로 원본의 첫 번째 칸은 그대로 남는다.
따라서 “얕은 복사는 원본을 바꾼다”라고 외우기보다 어떤 객체에 변경 연산을 했는지를 보는 편이 정확하다. copied[0] = ...와 copied[0].append(...)는 수정하는 대상이 다르다.
deepcopy도 내부 공유 관계는 유지할 수 있다
from copy import deepcopy
original = [[1, 2], [3, 4]]
copied = deepcopy(original)
copied[0].append(9)
print(original) # [[1, 2], [3, 4]]
print(copied) # [[1, 2, 9], [3, 4]]
print(copied[0] is original[0]) # False
이 예제에서는 내부 리스트도 복사돼 원본과 사본이 분리된다. 다만 원본 안에서 같은 객체를 여러 번 참조했다면 그 관계까지 없애지는 않는다.
from copy import deepcopy
shared = [0]
original = [shared, shared]
copied = deepcopy(original)
print(copied[0] is original[0]) # False
print(copied[0] is copied[1]) # True
deepcopy()는 한 번의 복사 과정에서 이미 처리한 객체를 기록한다. 그래서 위 예제의 사본도 동일한 내부 리스트를 두 번 참조한다. 사본의 두 행을 서로 독립적인 행으로 만들려면 복사 여부뿐 아니라 자료구조를 만드는 방식부터 확인해야 한다.
repeated = [[0] * 2] * 3
repeated[0][0] = 7
print(repeated) # [[7, 0], [7, 0], [7, 0]]
independent = [[0] * 2 for _ in range(3)]
independent[0][0] = 7
print(independent) # [[7, 0], [0, 0], [0, 0]]
필요한 독립성에 맞춰 고르기
| 목적 | 선택 | 확인할 점 |
| 같은 객체에 다른 이름 사용 | 대입 b = a | 어느 이름으로 수정해도 같은 객체에 반영 |
| 원소 추가·삭제·교체만 독립적으로 처리 | 얕은 복사 a.copy() | 내부의 변경 가능한 객체는 공유 |
| 중첩 리스트까지 원본과 분리 | deepcopy(a) | 원본 내부의 공유 관계는 사본에도 남을 수 있음 |
| 독립적인 2차원 리스트 생성 | 리스트 컴프리헨션 | 반복마다 내부 리스트를 새로 만들기 |
deepcopy()는 필요 이상으로 많은 데이터를 복사할 수 있고, 사용자 정의 클래스는 복사 동작을 직접 바꿀 수도 있다. 파일이나 소켓 같은 외부 자원까지 일반적인 중첩 리스트처럼 복제하는 도구는 아니다. 속도나 메모리 비용은 실제 데이터로 확인해야 하며, 여기서는 성능 수치를 비교하지 않았다.
원본과 사본의 경계를 확인할 때는 바깥 객체의 is 결과와 실제로 수정할 내부 객체의 is 결과를 함께 출력해 본다. 같은 내용이 보인다는 사실만으로 복사가 끝났다고 판단하지 않는 것이 핵심이다.
'언어별 개념 정리 > Python' 카테고리의 다른 글
| Python argparse에서 False가 True로 읽히는 이유 (0) | 2026.09.15 |
|---|---|
| pandas merge 전에 확인할 중복 키·결측 키와 행 수 (1) | 2026.09.12 |
| dataclass에서 mutable default 오류가 나는 이유와 해결법 (0) | 2026.09.08 |
| asyncio gather와 TaskGroup 비교: 예외가 발생하면 무엇이 달라질까? (0) | 2026.09.06 |
| Python uv로 가상환경과 의존성 잠금까지 한 번에 관리하기 (0) | 2026.09.03 |
댓글