객체를 다루다보면, 특정 객체를 복사해서 사용하는 경우가 종종 있다.
보통 이 과정에서는 단순하게 아래처럼 사용하고는 한다.
num = 150
num2 = num
print(num2) # 150
DFS와 백트래킹 관련 알고리즘 문제를 풀던 중 입력된 field(이중 list)를 복사해두어서 백트래킹 과정에서 사용했었다.
이 과정에서 단순하게 복사를 해서 사용했고, 의도대로 백트래킹 로직이 돌지 않았다는 것을 확인했다.
그리고 원인 파악에서 복사한 이중 list에 대한 원본이 계속 수정되고 있다는 것을 알게 되었다.
이 과정에서 Python의 얕은 복사와 깊은 복사에 대해 알게 되었고, 이를 정리하고자 한다.
얕은 복사(Shallow Copy)
새 객체를 생성하지만 원본에 포함된 객체에 대한 참조를 유지하는 방식
이중 list와 같은 구조에서는 최상위 객체에 대해서만 새롭게 복사하지만, 하위 객체인 내부 list에 대해서는 원본을 참조하는 것이라고 볼 수 있다.
Python에서는 기본적으로 얕은 복사를 사용한다. 대표적으로 아래와 같이 수행할 수 있고, copy 모듈의 copy()를 통해서도 수행할 수 있다.
an = 150
al = [[1,2,3],[4,5,6],[7,8,9]]
bn = an
bl = al[:]
import copy
c = copy.copy(a)
원본에 대한 참조가 유지되기에 사본에 대한 변경이 원본에 반영되는 것이 특징이다.
a = [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]
# 얕은 복사
c = a[:]
# 복사본 객체의 변경
c[0][0] = 99
# 원본에도 변경이 이루어짐
print(a)
# [[99, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]
print(c)
# [[99, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]
이러한 얕은 복사는 어디까지나 원본의 최상위 객체만 새롭게 복사하고, 하위 객체는 참조를 하기에 아래와 같은 상황도 발생할 수 있다.
a = [{'a': 1, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]
# 얕은 복사
c = a[:]
# 복사본 객체의 변경 => 최상위 객체(list) 변경
d = c.pop(0)
# 최상위 객체의 수정이므로 원본에는 변경없음
print(a)
# [{'a': 1, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]
print(c)
# [{'d': 4, 'e': 5, 'f': 6}]
복사된 객체의 최상위 요소인 list에 대해 수정했기 때문에, 원본은 변경없이 그대로인 것이다.
그리고 이후 복사된 list에서 pop한 객체(d)를 수정하면 남아있는 참조에 따라 원본이 변경되는 것을 볼 수 있다.
# 사본의 하위 객체(dict)의 변경
d['a'] = 808
# 원본에 대한 참조는 아직 유효하기 때문에 원본의 dict가 변경
print(a)
# [{'a': 808, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]
print(d)
# {'a': 808, 'b': 2, 'c': 3}
깊은 복사(Deep Copy)
원본 객체에 대한 완전히 독립적인 복사된 객체를 생성하는 방식
새 객체를 생성한 뒤, 원본에서 발견한 자식 객체 항목들을 재귀적으로 채우는 방식으로 원본과 독립된 객체를 생성한다.
깊은 복사는 copy 모듈의 deepcopy를 통해 수행할 수 있다.
깊은 복사는 원본과 완전히 독립된 객체를 생성하기에, 복사본이나 원본의 변경에도 반대의 것에는 영향을 주지 않는다.
import copy
a = [{'a': 1, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]
b = copy.deepcopy(a)
b[0]['a'] = 999
# 원본과 완전히 독립된 객체를 생성했기에, 복사본의 변경 결과는 원본에 영향이 없음
print(a)
# [{'a': 1, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]
print(b)
# [{'a': 999, 'b': 2, 'c': 3}, {'d': 4, 'e': 5, 'f': 6}]