파이썬에서 변수는 단순히 객체에 대한 참조(reference)일 뿐입니다. 따라서 한 변수를 다른 변수에 할당하면 객체가 복사되는 것이 아니라, 같은 객체를 가리키는 또 하나의 참조만 만들어집니다. 이는 내장 함수인 id()를 통해 확인할 수 있습니다.
>>> L1 = [1,2,3]
>>> L2 = L1
>>> id(L1), id(L2)
(2165544063496, 2165544063496)
위 코드의 실행 결과에서 두 리스트 객체의 id() 값이 동일한 것을 확인할 수 있습니다. 즉, L1과 L2는 같은 객체를 가리키고 있으며, 이때 L2를 L1의 얕은 복사(shallow copy)라고 합니다. 두 변수가 같은 객체를 참조하고 있기 때문에 어느 한쪽에서 변경을 가하면 그 변경이 다른 쪽에도 그대로 반영됩니다.
>>> L1 = [1,2,3]
>>> L2 = L1
>>> L2[1] = 100
>>> L1,L2
([1, 100, 3], [1, 100, 3])
위 예제에서는 L2의 인덱스 1에 해당하는 요소를 변경했습니다. 그 결과 변경 사항이 L1과 L2 양쪽 모두에 나타나는 것을 확인할 수 있습니다.
copy 모듈의 두 가지 복사 방법
복사 작업 시 완전히 새로운 객체가 생성되고, 원본에 포함된 중첩 객체(nested object)들까지 재귀적으로 복사하여 새 객체에 담는 방식을 깊은 복사(deep copy)라고 합니다.
파이썬 표준 라이브러리의 copy 모듈은 다음 두 가지 메서드를 제공합니다.
copy.copy()– 얕은 복사를 수행합니다.copy.deepcopy()– 깊은 복사를 수행합니다.
얕은 복사(copy.copy())의 특징
얕은 복사는 새로운 객체를 생성하고 원본 요소들의 참조를 저장하지만, 중첩된 객체 자체를 복사하지는 않습니다. 대신 중첩 객체에 대한 참조만 그대로 가져오기 때문에 복사 과정이 재귀적으로 이루어지지 않습니다.
>>> import copy
>>> L1 = [[1,2,3], [4,5,6]]
>>> L2 = copy.copy(L1)
>>> L1,L2
([[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]])
>>> L2.append([10,10,10])
>>> L1,L2
([[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6], [10, 10, 10]])
여기서 L1은 중첩 리스트이며, L2는 이것의 얕은 복사본입니다. 부모 리스트 객체에 대한 참조는 L2로 복사되었지만, 내부의 중첩 요소들은 복사되지 않았습니다. 따라서 L2에 새로운 리스트를 추가해도 그 변화는 L1에는 영향을 주지 않습니다.
그러나 자식 요소(중첩된 내부 리스트)의 값을 수정하려고 하면, 그 효과는 두 리스트 모두에 나타납니다.
>>> L1 = [[1,2,3], [4,5,6]]
>>> L2 = copy.copy(L1)
>>> L1,L2
([[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]])
>>> L2[1][0] = 100
>>> L1,L2
([[1, 2, 3], [100, 5, 6]], [[1, 2, 3], [100, 5, 6]])
깊은 복사(copy.deepcopy())의 특징
반면 깊은 복사는 새로운 객체를 생성할 때, 원본 요소에 포함된 중첩 객체들까지 재귀적으로 복사하여 추가합니다.
다음 예제에서 L2는 L1의 깊은 복사본입니다. 이 상태에서 내부 리스트의 어떤 요소를 변경하더라도, 그 변경은 다른 리스트에는 전혀 영향을 미치지 않습니다.
>>> L1 = [[1,2,3], [4,5,6]]
>>> L2 = copy.deepcopy(L1)
>>> L1,L2
([[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]])
>>> L2[1][0] = 100
>>> L1,L2
([[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [100, 5, 6]])
실행 결과를 통해 깊은 복사가 원본과 복사본을 완전히 독립적으로 분리한다는 것을 확인할 수 있습니다. 중첩된 데이터 구조를 다룰 때 의도치 않은 데이터 변경을 방지하려면 copy.deepcopy()를 사용하는 것이 안전합니다.