파이썬에서 shallow copy와 deep copy는 객체를 복제하는 방법들이지만, nested 객체의 복제를 처리하는 방식에서 차이가 있다.
일단, Immutable 객체에게 shallow copy와 deep copy를 따질 필요는 없다.
Immutable 객체(숫자, string, tuple 등)의 경우 copy의 종류를 따지는 것은 무의미하다.
왜냐하면 immutable 객체는 생성 후 변경이 불가능하기 때문이다. Immutable 객체 그 자체는 절대 변경될 수 없다. 따라서 같은 immutable 객체를 참조하는 곳 중 한 군데에서 immutable의 객체 값을 수정해서 다른 쪽에 영향을 줄 수 있는 상황 자체가 없는 것이다. 예를 들어 만약 immutable 객체를 담은 변수에 새로운 immutable 객체를 할당할 경우, 변수에는 그 새로운 객체에 대한 reference가 새로 저장될 뿐이다.
참고로, immutable 객체를 copy하면, 파이썬은 메모리 사용의 최적화를 위해, 새로운 메모리 할당을 하지 않고 동일한 immutable 객체에 대한 reference를 복사한다. Immutable 객체는 어차피 수정될 일이 없으므로, 모든 복사된 곳에서 동일한 reference를 갖도록 하는 것이다.
이를 immutable 객체인 tuple과 Shallow copy와 deep copy를 지원하는 파이썬 모듈인 copy를 통해 확인해보자.
import copy
# Immutable 객체 (tuple)
immutable_obj = (1, 2, 3)
# Shallow copy
shallow_copy = copy.copy(immutable_obj)
# Deep copy
deep_copy = copy.deepcopy(immutable_obj)
# 동일한 객체인지 확인
print(immutable_obj is shallow_copy)
print(immutable_obj is deep_copy)
True
True
Shallow copy이든 deep copy이든 상관없이, 원래의 immutable 객체에 대한 reference를 가지고 있음을 확인할 수 있다.
다만 원본 immutable 객체 안에 list 같은 mutable 객체가 있으면, deep copy의 경우 새로운 메모리 할당이 발생한다.
import copy
# Mutable element(list)를 가진 원본 tuple
original = ([1, 2], [3, 4])
shallow = copy.copy(original)
print(original is shallow)
deep = copy.deepcopy(original)
print(original is deep)
deep[0][0] = 99
print("Original:", original)
print("Shallow Copy:", shallow)
print("Deep Copy:", deep)
True
False
Original: ([1, 2], [3, 4])
Shallow Copy: ([1, 2], [3, 4])
Deep Copy: ([99, 2], [3, 4])
애초에 위처럼 tuple 안에 list를 넣는 코드는 tuple의 불변성과 list의 가변성이 혼합되어 버린다는 점에서 피하는 것이 좋다.
Mutable 객체에 대해서는 shallow copy와 deep copy를 따져봐야 한다.
List나 dict와 같은 mutable 객체들에 있어서 shallow copy와 deep copy는 매우 큰 차이가 있다.
왜냐하면 nested 객체를 포함하는 mutable 객체를 복제하는 경우, 한쪽의 수정은 다른 쪽에 영향을 줄 수 있기 때문이다.
Python에서 "Shallow Copy"란?
- Shallow copy는 새로운 collection 객체(list, dict, set 등)가 생성되고, 기존 객체의 자식 객체들에 대한 reference들이 그 collection 객체를 채우는 것을 의미한다.
- Shallow copy의 순서는 다음과 같다:
- 먼저 Top-level 컨테이너(새로운 list, 새로운 dict 등)이 생성된다.
- 생성된 Top-level 컨테이너 안의 element들은 기존 객체의 자식 객체이 복제 생성된 것이 아니라, 기존 객체의 자식 객체들에 대한 reference들이 복제된 것이다.
- Nested 객체들(예를 들면 list 안의 list)는 복제되지 않는다. 대신 해당 nested 객체들에 대한 reference가 복제된다.
- 따라서 shallow copy는 "one-level deep"이다. 최상위 객체만이 기존 객체와 독립된 메모리 공간에 위치한 별개의 객체이다.
- Copy 과정은 recursive하지 않으므로 기존 객체의 자식 객체들이 복제 생성되는 일은 없다.
Python에서 "Deep Copy"란?
- Deep copy는 새로운 collection 객체(list, tuple, dict, set 등)가 생성되고, copy 과정을 recursive하게 수행한다.
- Deep copy의 순서는 다음과 같다:
- 먼저 Top-level 컨테이너(새로운 list, 새로운 tuple, 또는 새로운 dict 등)이 생성된다.
- 생성된 Top-level 컨테이어를 기존 객체의 자식객체들을 완전히 새로 복제한 것(clone)으로 recursive하게 채운다.
- 이렇게 하면 전체 객체 tree를 순회하면서 기존 객체와 그 자식 객체들로부터 완전히 독립된 clone을 생성하게 된다.
다중 List의 copy 사례를 통해 shallow copy와 deep copy의 원리를 더 이해해 보자.
아래 코드를 보자.
sample = [10, 20]
l = [1, 2, 3, sample, 4, 5, 6]
ll = [1, 2, 3, sample, 4, 5, 6]
목표는 다중 list인 l과 ll의 element 중 하나로 sample을 초기 저장하되, 이후에는 각자가 자신의 sample list를 별도로 관리할 수 있도록 하는 것이다.
핵심은 l의 sample 과 ll의 sample이 완전히 별도로 분리되어서, 한쪽을 수정할 때 다른 쪽이 영향받지 않도록 하는 것이다.
시도 1. List의 copy() 메소드 사용
List의 내장 함수인 copy()를 사용해본다.
sample = [10, 20]
l = [1, 2, 3, sample, 4, 5, 6]
ll = l.copy()
sample[0] = 1000
print(l, ll)
[1, 2, 3, [1000, 20], 4, 5, 6] [1, 2, 3, [1000, 20], 4, 5, 6]
sample에 대한 수정이 양쪽 list 모두에 영향을 끼치고 있으므로, 복사가 의도한대로 동작하지 않았다.
원인은 list 내장함수인 copy는 shallow copy만을 지원하기 때문이다.
List l과 ll의 주소를 살펴보면 다른 것을 확인할 수 있다. 즉 최상위 객체인 바깥 list는 복제 생성이 됬음을 알 수 있다.
print(hex(id(l)), hex(id(ll)))
0x16e25236800 0x16e25242980 # 양쪽이 다름
반면 l과 ll의 각 element의 주소를 index별로 비교해보면, 모두 양쪽 주소가 동일함을 볼 수 있다.
즉 list의 자식 객체들에 대한 reference가 그대로 복사되었고, 자식 객체들의 clone이 복제 생성된 것은 아님을 알 수 있다.
for i in range(len(l)):
print(hex(id(l[i])), hex(id(ll[i])))
0x7ffebe0f4328 0x7ffebe0f4328 # 양쪽이 동일
0x7ffebe0f4348 0x7ffebe0f4348 # 양쪽이 동일
0x7ffebe0f4368 0x7ffebe0f4368 # 양쪽이 동일
0x16e252432c0 0x16e252432c0 # 양쪽이 동일
0x7ffebe0f4388 0x7ffebe0f4388 # 양쪽이 동일
0x7ffebe0f43a8 0x7ffebe0f43a8 # 양쪽이 동일
0x7ffebe0f43c8 0x7ffebe0f43c8 # 양쪽이 동일
코드 실행 후 메모리 상태를 그림으로 살펴보자. (pythontutor.com 사용)
l과 ll이 별도의 list 객체로 메모리에 존재하고 있다.
l.copy()는 l의 shallow copy를 생성한다.
- 새로운 list 객체가 생성된다.
- l의 element들이 새로운 list 객체의 element로 복사된다(값이 아닌 reference의 복사).
- immutable 객체(int, str, tuple 등)은 값이 변하지 않으므로 이것이 문제가 안된다.
- 반면 mutable 객체(list, dict, object 등)은 문제가 된다. Reference가 그대로 복사되므로 mutable 객체의 메모리 공간을 reference가 그대로 가리키고 있기 때문이다. Mutable 객체는 내부 값이 변할 수 있으므로 한쪽 reference를 통해 수정된 값을 다른 reference를 통해 그대로 접근 가능하다.
시도 2. 내부 list에 copy() 메소드 사용
List l에 미리 sample의 복사본을 생성해두는 방법을 써본다.
sample = [10, 20]
l = [1, 2, 3, sample.copy(), 4, 5, 6]
ll = l.copy()
sample[0] = 1000
l[3][0] = 10000
print(l, ll)
[1, 2, 3, [10000, 20], 4, 5, 6] [1, 2, 3, [10000, 20], 4, 5, 6]
시도 1에서의 방식대로 sample에 대한 수정을 가했을 때 양쪽 list 모두 영향을 받지 않았음을 알 수 있다.
그러나 l[3][0] = 10000 문이 l과 ll 양쪽의 내부 list에 동일하게 수정을 가한 것을 통해, 여전히 l과 ll은 동일한 list 객체를 공유하고 있음을 알 수 있다.
원인은 이번에도 shallow copy만이 수행되었기 때문이다.
List l과 ll의 주소를 살펴보면 다른 것을 확인할 수 있다. 즉 최상위 객체인 바깥 list는 복제 생성이 됬음을 알 수 있다.
print(hex(id(l)), hex(id(ll)))
0x261e4722f40 0x261e4722a40 # 양쪽이 다름
l과 ll의 각 element의 주소를 index별로 비교해보면, 모두 양쪽 주소가 동일함을 볼 수 있다.
즉 list의 자식 객체들에 대한 reference가 그대로 복사되었고, 자식 객체들의 clone이 복제 생성된 것은 아님을 알 수 있다.
for i in range(len(l)):
print(hex(id(l[i])), hex(id(ll[i])))
0x7ffebe0f4328 0x7ffebe0f4328 # 양쪽이 동일
0x7ffebe0f4348 0x7ffebe0f4348 # 양쪽이 동일
0x7ffebe0f4368 0x7ffebe0f4368 # 양쪽이 동일
0x261e4716800 0x261e4716800 # 양쪽이 동일
0x7ffebe0f4388 0x7ffebe0f4388 # 양쪽이 동일
0x7ffebe0f43a8 0x7ffebe0f43a8 # 양쪽이 동일
0x7ffebe0f43c8 0x7ffebe0f43c8 # 양쪽이 동일
코드 실행 후 메모리 상태를 그림으로 살펴보자. (pythontutor.com 사용)
l의 내부 list는 sample로부터 shallow copy된 객체이고, ll은 l이 shallow copy된 객체이다.
그러므로 sample과 l 및 ll 의 내부 list는 완전히 분리된 객체이고, l과 ll 이 별도의 list 객체로 메모리에 존재하고 있지만, l과 ll의 내부 list는 여전히 동일한 list를 reference하고 있는 것을 알 수 있다.
시도 3. copy 모듈의 deepcopy() 사용
copy 모듈을 import해서 deepcopy() 함수를 사용해본다.
이제는 l도 (sample을 내부 list로 갖는) 임의의 list에 대해 deepcopy() 함수를 호출한 객체이다. ll은 l에 대해 deepcopy() 함수를 호출한 객체이다.
import copy
sample = [10, 20]
l = copy.deepcopy([1, 2, 3, sample, 4, 5, 6])
ll = copy.deepcopy(l)
sample[0] = 1000
l[3][0] = 10000
print(l, ll)
([1, 2, 3, [10000, 20], 4, 5, 6], [1, 2, 3, [10, 20], 4, 5, 6])
sample에 대한 수정이 l의 내부 list와 ll의 내부 list에 영향을 주지 않았고, l의 내부 list에 대한 수정이 ll의 내부 list에 영향을 주지 않았음을 확인할 수 있다.
즉 이번에는 다중 list의 deep copy가 수행되었다.
List l과 ll의 주소를 살펴보면 다른 것을 확인할 수 있다. 최상위 객체인 바깥 리스트는 복제 생성이 됐다는 뜻이고, 이것은 shallow copy 때와 동일하다.
print(hex(id(l)), hex(id(ll)))
0x24245fd6340 0x24245fb45c0
l과 ll의 각 element의 주소를 index별로 비교해보면, 이번에는 내부 list의 주소가 다른 것을 볼 수 있다.
즉 sample과 l의 내부 list와 ll의 내부 list가 모두 별도의 메모리 공간에 위치한다.
(참고로 deep copy가 적용됬음에도 내부 list를 제외한 다른 element들은 주소값이 동일하다. 이는 integer interning 때문이다. 관련 내용은 https://comgu.tistory.com/entry/Python-List-곱하기-연산의-메모리-구조-분석1차원-list-다중-list 글에서 다루었다.)
for i in range(len(l)):
print(hex(id(l[i])), hex(id(ll[i])))
0x7ffebe0f4328 0x7ffebe0f4328 # 양쪽이 동일
0x7ffebe0f4348 0x7ffebe0f4348 # 양쪽이 동일
0x7ffebe0f4368 0x7ffebe0f4368 # 양쪽이 동일
0x24245fd6f00 0x24245facbc0 # 양쪽이 다름
0x7ffebe0f4388 0x7ffebe0f4388 # 양쪽이 동일
0x7ffebe0f43a8 0x7ffebe0f43a8 # 양쪽이 동일
0x7ffebe0f43c8 0x7ffebe0f43c8 # 양쪽이 동일
코드 실행 후 메모리 상태를 그림으로 살펴보자. (pythontutor.com 사용)
sample, l, ll이 모두 완벽하게 분리된 메모리 공간에 위치하게 된 것을 확인 가능하다.
Deep copy를 통해서 list의 모든 element들이 recursive하게 복제 생성되었음을 알 수 있다.
참고: https://realpython.com/copying-python-objects/
Shallow vs Deep Copying of Python Objects – Real Python
What's the difference between a shallow and a deep copy of a Python object? Learn how to clone arbitrary objects in Python, including your own custom classes.
realpython.com
'Python > 문법' 카테고리의 다른 글
[Python] 데코레이터(Decorator) 1 - 데코레이터, 중첩 데코레이터 (0) | 2024.12.11 |
---|---|
[Python] 클로저(Closure) 함수 (0) | 2024.12.11 |
[Python] List 곱하기 연산의 메모리 구조 분석(1차원 list, 다중 list) (1) | 2024.12.09 |
[Python] Asterisk(*)가 pack과 unpack을 수행하는 방식 (2) | 2024.12.06 |
[Python] Static Method (@staticmethod) (0) | 2024.12.05 |