리스트를 다른 변수에 대입한 뒤 수정했을 때 원본까지 바뀌는 경우가 있다. 이때 확인할 것은 변수 이름이 아니라 두 변수가 가리키는 객체다. 대입과 얕은 복사, 깊은 복사를 같은 데이터로 비교해 정리한다.
original = [10, 20, 30]
assigned = original
assigned.append(40)
print(original) # [10, 20, 30, 40]
print(assigned is original) # True
assigned = original은 리스트를 복제하는 명령이 아니다. 기존 리스트에 assigned라는 이름을 연결한다. 따라서 어느 이름으로 접근하든 같은 리스트를 수정하게 된다.
여기서 is는 같은 객체인지를 검사한다. 값이 같은지를 비교하는 ==와 구분해야 한다. 내용이 같아도 서로 다른 리스트일 수 있다.
original = [10, 20, 30]
copied = original.copy()
sliced = original[:]
copied.append(40)
sliced[0] = 99
print(original) # [10, 20, 30]
print(copied) # [10, 20, 30, 40]
print(sliced) # [99, 20, 30]
print(copied is original) # False
print(sliced is original) # False
두 방법 모두 바깥쪽 리스트를 새로 만든다. 복사한 리스트에 원소를 추가하거나 특정 위치의 값을 교체해도 원본 리스트의 구조는 바뀌지 않는다.
이번 예제의 원소는 변경할 수 없는 정수다. sliced[0] = 99는 정수 10 자체를 수정하는 것이 아니라, 새 리스트의 첫 번째 위치가 참조할 객체를 바꾸는 연산이다.
original = [[10, 20], [30, 40]]
copied = original.copy()
copied[0].append(99)
print(original) # [[10, 20, 99], [30, 40]]
print(copied is original) # False
print(copied[0] is original[0]) # True
바깥쪽 리스트는 서로 다르지만, 첫 번째 원소인 내부 리스트는 공유한다. copied[0].append(99)는 공유 중인 내부 리스트를 수정하므로 원본에서도 변경이 보인다. 전체 슬라이싱으로 복사해도 같은 점에 주의해야 한다.
이것이 얕은 복사다. ‘복사했으니 모든 값이 독립적이다’라고 판단하기보다 어느 단계까지 새 객체가 만들어졌는지 확인하는 편이 정확하다.
from copy import deepcopy
original = [[10, 20], [30, 40]]
copied = deepcopy(original)
copied[0].append(99)
print(original) # [[10, 20], [30, 40]]
print(copied) # [[10, 20, 99], [30, 40]]
print(copied[0] is original[0]) # False
이 예제에서는 deepcopy()가 내부 리스트까지 복사하므로 원본을 유지할 수 있다. 다만 모든 상황에서 깊은 복사를 사용할 필요는 없다. 복사 범위가 커지면 비용도 늘어나므로, 실제로 어떤 객체를 변경할 것인지 먼저 정해야 한다.
| 방법 | 바깥 리스트 | 중첩된 내부 리스트 | 적합한 상황 |
|---|---|---|---|
b = a | 공유 | 공유 | 같은 대상을 여러 이름으로 참조 |
a.copy() | 새로 생성 | 공유 | 바깥 리스트만 독립적으로 변경 |
a[:] | 새로 생성 | 공유 | 리스트 전체를 얕게 복사 |
deepcopy(a) | 새로 생성 | 이 예제에서는 별도 복사 | 중첩 리스트까지 독립적으로 변경 |
원본을 보존해야 한다면 ‘복사 여부’와 ‘복사 깊이’를 나누어 확인한다. 특히 내부 리스트를 수정하는 작업에서는 바깥 객체가 다르다는 사실만으로 안전하다고 판단하면 안 된다.
본문의 네 코드 블록은 각각 독립된 예제이며, Python에서 실행해 주석의 출력값을 확인했다.