아래 링크된 글을 먼저 읽고 immutable 객체와 mutable 객체의 개념에 대해 이해하는 것을 추천한다.
https://youngandmini.tistory.com/7
[Python] 파이썬 변수가 객체를 참조하는 방식 (immutable 객체와 mutable 객체)
파이썬에서는 immutable 객체인지 mutable 객체인지에 따라 참조하는 방식이 다르다. immutable 객체: 한 번 생성되면 값을 변경할 수 없는 객체. int, float, str 같은 자료형의 객체는 한 번 객체가 생성되
youngandmini.tistory.com
깊은 복사(deep copy) vs 얕은 복사(shallow copy)
파이썬의 슬라이싱(list[:])이 깊은 복사인지 얕은 복사인지를 알아보기 전에, 먼저 깊은 복사와 얕은 복사가 무엇인지를 알아야한다.
깊은 복사란?
깊은 복사란 객체의 값을 복사하여, 아예 새로운 메모리에 할당하고 그 객체의 참조를 얻는 것이다. 아래 그림처럼 리스트 A를 깊은 복사하여 리스트 B를 만들라는 명령을 받으면, 파이썬은 새로운 메모리 공간을 할당하고 리스트 A가 참조하는 값들을 하나씩 복사하여 새로 할당한 메모리 공간에 저장한다. 그리고 그 새로운 값들을 리스트 B가 참조하도록 한다.
그렇기 때문에, 깊은 복사된 리스트의 요소들은 기존의 요소들과는 서로 독립된 객체이고, 따라서 두 리스트의 변경이 서로에게 전달되지 않는다.
깊은 복사를 아래 파이썬 코드를 보면서 이해해 보자.
리스트 a를 깊은 복사하여 리스트 b를 생성했다. 리스트 a의 inner_list의 주소값과, 리스트 b의 inner_list의 주소값이 서로 다른 것을 확인할 수 있다. 새로운 메모리를 할당하여 inner_list의 값을 복사했기 때문이다.
리스트 a의 inner_list와, 리스트 b의 inner_list가 서로 다른 메모리 공간에 저장되어 있기 때문에, 리스트 b에서의 변경은 리스트 a로 전달되지 않는 것도 확인할 수 있다.
import copy
inner_list = [1,2,3]
a = [inner_list]
b = copy.deepcopy(a)
print(a, b) # [[1, 2, 3]] [[1, 2, 3]]
print(id(a[0]), id(b[0])) # 2335706396288 2335707143552
b[0][0] = 999
print(a, b) # [[1, 2, 3]] [[999, 2, 3]]
얕은 복사란?
얕은 복사란 깊은 복사와는 반대로 객체를 새로 생성하지 않고, 그 객체의 주소값을 복사하여 참조하도록 한다. 아래 그림처럼 리스트 A를 얕은 복사하여 리스트 B를 만들라는 명령을 받으면, 파이썬은 리스트 A가 참조하는 요소들을 그대로 리스트 B가 참조하도록 만든다.
그렇기 때문에, 얕은 복사된 리스트의 요소들은 사실 같은 객체이고, 따라서 한 리스트가 변경되면 다른 리스트도 똑같이 변경되게 된다.
얕은 복사를 아래 파이썬 코드를 보면서 이해해 보자.
깊은 복사를 했던 때와 달리, 이번에는 리스트 a의 inner_list의 주소값과, 리스트 b의 inner_list의 주소값이 서로 같은 것을 확인할 수 있다. 두 요소가 같은 객체라는 것을 의미한다.
같은 객체이기 때문에, 한쪽 리스트의 요소를 변경하였을 때, 다른 리스트의 요소도 함께 변경되는 것도 확인할 수 있다.
import copy
inner_list = [1,2,3]
a = [inner_list]
b = copy.copy(a)
print(a, b) # [[1, 2, 3]] [[1, 2, 3]]
print(id(a[0]), id(b[0])) # 2197666346432 2197666346432
b[0][0] = 999
print(a, b) # [[999, 2, 3]] [[999, 2, 3]]
파이썬의 슬라이싱은 깊은 복사일까, 얕은 복사일까?
드디어 파이썬의 슬라이싱이 깊은 복사인지 얕은 복사인지를 알 준비가 되었다. 결과적으로 말하면 파이썬의 슬라이싱은 얕은 복사이다!
간단하게 코드로 확인해보자. 앞선 예시 코드를 복사해와서 copy.copy(a)를 사용하는 대신 슬라이싱을 사용했을 때, 결과가 얕은 복사의 경우와 동일한 것을 확인할 수 있다.
inner_list = [1,2,3]
a = [inner_list]
b = a[:]
print(a, b) # [[1, 2, 3]] [[1, 2, 3]]
print(id(a[0]), id(b[0])) # 2934068866176 2934068866176
b[0][0] = 999
print(a, b) # [[999, 2, 3]] [[999, 2, 3]]
무엇보다도, 파이썬 공식 문서에서도 아래와 같이 슬라이싱을 얕은 복사로 소개하고 있다.
Shallow copies of dictionaries can be made using dict.copy(), and of lists by assigning a slice of the entire list, for example, copied_list = original_list[:].
https://docs.python.org/3/library/copy.html
copy — Shallow and deep copy operations
Source code: Lib/copy.py Assignment statements in Python do not copy objects, they create bindings between a target and an object. For collections that are mutable or contain mutable items, a copy ...
docs.python.org
파이썬의 깊은 복사와 얕은 복사가 헷갈리는 이유
파이썬의 슬라이싱이 깊은 복사인지 얕은 복사인지를 확인하기 위해 정보를 찾던 중, 슬라이싱을 깊은 복사로 소개하고 있는 글이 있었다. 충분히 헷갈릴 여지가 있는 문제이다.
아래 코드를 보자. 깊은 복사인지 얕은 복사인지와 상관 없이, 모두 같은 주소값을 참조하고 있는 것을 볼 수 있다. 그러나 한 리스트의 요소를 변경했을 때에는 다른 리스트에 변화가 전달되지 않는다. 뭔가 이상하다. 요소의 주소값이 바뀌지 않는다(모든 리스트가 같은 객체를 참조한다)는 것에 집중하면 얕은 복사처럼 보이고, 한 리스트의 변경이 다른 리스트에 전달되지 않는다는 것에 집중하면 깊은 복사처럼 보인다. 왜 그럴까?
import copy
a = [1,2,3]
b = a[:]
c = copy.copy(a)
d = copy.deepcopy(a)
print(a, b, c, d) # [1, 2, 3] [1, 2, 3] [1, 2, 3] [1, 2, 3]
print(id(a[0]), id(b[0]), id(c[0]), id(d[0])) # 140714349500064 140714349500064 140714349500064 140714349500064
b[0] = 999
c[0] = 888
d[0] = 777
print(a, b, c, d) # [1, 2, 3] [999, 2, 3] [888, 2, 3] [777, 2, 3]
print(id(a[0]), id(b[0]), id(c[0]), id(d[0])) # 140714349500064 1998059370864 1998064215920 1998064215888
위와 같은 문제가 발생하는 이유는 바로 파이썬의 immutable 객체를 참조하는 방식과 그 특이성 때문이다. (파이썬이 immutable 객체를 참조하는 방식은 https://youngandmini.tistory.com/7를 참고하도록 하자.)
immutable 객체인 int 형 객체는 Object Interning에 의해 동일한 값에 대해서는 동일한 객체를 참조하도록 한다. 그렇기 때문에 아예 새로운 객체를 만들어 참조하는 깊은 복사의 경우에도, int형 객체를 새로 생성하지 않고 동일한 객체를 참조한다.
리스트의 요소를 변경할 때에도 immutable 객체의 특징이 그대로 반영된다. immutable 객체는 말 그대로 값이 변하지 않는 객체이다. 따라서 immutable 객체의 값을 변경하라는 명령을 받으면, 값을 변경하는 대신 기존의 참조를 끊고 새로운 객체를 생성하여 참조한다. 그래서 위 코드에서 리스트 요소의 값을 변경했을 때에는, 그 요소의 주소값이 달라진 것을 확인할 수 있다.
이제는 파이썬의 깊은 복사와 얕은 복사가 헷갈리는 이유가, 파이썬이 immutable 객체를 참조하는 방식의 특이성 때문임을 알았으니 헷갈리지 않도록 하자!
'프로그래밍 언어 > Python' 카테고리의 다른 글
[Python] 파이썬 변수가 객체를 참조하는 방식 (immutable 객체와 mutable 객체) (0) | 2023.08.10 |
---|