파이썬에는 얕은 복사와 깊은 복사가 있는데, 이것을 제대로 이해하기 위해서는 파이썬에 있는 mutable 객체, immutable 객체에 대해서 알아야 한다.
mutable 객체와 immutable 객체에 대하여
파이썬에서는 객체의 종류를 두 가지로 구분할 수 있는데,
mutable 은 변경되는 객체이고 종류는 list, set, dictionary 가 있고
immutable 은 변경되지 않는 객체로 종류는 int, float, tuple, str, bool 이 있다.
immutable
mutable 객체의 경우에는 모든 객체를 각각 생성해서 참조하고, immutable 객체는 값이 같은 경우에 변수에 상관없이 동일한 곳을 참조한다. 예제를 통해서 이해해보자. immutable 객체인 int 형 변수에 값을 선언하고 주소값을 출력한 것이다.
a = 10
b = 10
c = 10
print("a 값: {0}, 주소값: {1}".format(a, hex(id(a))))
print("b 값: {0}, 주소값: {1}".format(b, hex(id(b))))
print("c 값: {0}, 주소값: {1}".format(c, hex(id(c))))
a 값: 10, 주소값: 0x1e6a9516a50
b 값: 10, 주소값: 0x1e6a9516a50
c 값: 10, 주소값: 0x1e6a9516a50
보통 c 언어 에서는 각 변수마다 메모리를 줘서 그 주소 값이 다른데, 파이썬에서는 다른 방식이다. 실행결과를 보면 변수 a, b, c 의 주소값이 모두 같은 것을 볼 수 있다. 10이라는 값이 존재하는 메모리 주소를 세 변수가 모두 참조하고 있는 것이다. 이렇게 하면 굳이 변수 10을 n개 만들 필요가 없어진다.
그렇다면 값이 바뀐다면 어떻게 될까?
a = 10
b = 10
print("a 값: {0}, 주소값: {1}".format(a, hex(id(a))))
print("b 값: {0}, 주소값: {1}".format(b, hex(id(b))))
a = 20
b = 20
print("a 값: {0}, 주소값: {1}".format(a, hex(id(a))))
print("b 값: {0}, 주소값: {1}".format(b, hex(id(b))))
a 값: 10, 주소값: 0x1e6a9516a50
b 값: 10, 주소값: 0x1e6a9516a50
a 값: 20, 주소값: 0x1e6a9516b90
b 값: 20, 주소값: 0x1e6a9516b90
값이 바뀜에 따라 주소값도 바뀌는 것을 볼 수 있다. 값이 20인 객체를 생성해서 이것을 참조하도록 하는 것이다.
하지만 str 타입의 경우에는 조금 다르다.
s1 = "HelloWorld"
s2 = "HelloWorld"
print("s1 값: {0}, 주소값: {1}".format(s1, hex(id(s1))))
print("s2 값: {0}, 주소값: {1}".format(s2, hex(id(s2))))
s1 = s1.replace('H', 'hh')
s2 = 'hhelloWorld'
print("s1 값: {0}, 주소값: {1}".format(s1, hex(id(s1))))
print("s2 값: {0}, 주소값: {1}".format(s2, hex(id(s2))))
s1 값: HelloWorld, 주소값: 0x1e6ab9d00f0
s2 값: HelloWorld, 주소값: 0x1e6ab9d00f0
s1 값: hhelloWorld, 주소값: 0x1e6ab9d2970
s2 값: hhelloWorld, 주소값: 0x1e6ab9de7f0
최초로 s1, s2 가 선언될 때에는 "HelloWorld" 라는 동일한 문자열을 참조하고 있는 것을 알 수 있다. 그러나 s1, s2 값을 각각 변경해서 '"hhelloWorld" 로 동일하게 세팅했지만 주소값이 다른 것을 볼 수 있다. str 같은 경우에는 항상 문자열이 같은지 보고 같은 곳을 참조할지 판단하기가 쉽지 않기 때문에 값이 같다고 항상 같은 곳을 참조하진 않는 것으로 보인다.
mutable
mutable 객체는 값이 변경될 수 있는 객체로, 모든 객체를 각각 생성해서 참조한다. 값이 모두 값은 리스트를 선언하고 주소값을 출력하는 예제이다.
arr1 = [1, 2, 3]
arr2 = [1, 2, 3]
arr3 = [1, 2, 3]
print("arr1 값: {0}, 주소값: {1}".format(arr1, hex(id(arr1))))
print("arr2 값: {0}, 주소값: {1}".format(arr2, hex(id(arr2))))
print("arr3 값: {0}, 주소값: {1}".format(arr3, hex(id(arr3))))
arr1 값: [1, 2, 3], 주소값: 0x1e6ab9d9980
arr2 값: [1, 2, 3], 주소값: 0x1e6ab9da800
arr3 값: [1, 2, 3], 주소값: 0x1e6ab9ccf40
결과를 보면 arr1, arr2, arr3 이 참조하는 [1, 2, 3] 이 모두 다른 주소인 것을 알 수 있다.
arr1.append 와 같이 mutuble 한 리스트는 값이 자유롭게 바뀔 수 있기 때문에 각각의 메모리를 할당해 주는게 관리가 더 용이하다고 판단한 것 같다.
그리고 예상할 수 있듯이 내부 값이 변한다 하더라도 참조하는 메모리 주소가 바뀌지 않고, 다른 객체에 영향을 끼치지 않는다.
mutable 객체와 immutable 객체
mutable 객체와 immutable 객체에 대해 그림으로 그려보면 이런 그림이 될 것이다.
immutable 한 int, float 등의 객체들은 사실 깊은 복사를 하던 얕은 복사를 하던 상관이 없다. 왜냐하면 해당 객체들은 값이 변경되면 무조건 참조가 변경되기 때문이다. 예를 들어, a = 10; b = a; 이렇게 b 에 a 를 얕은 복사하면 a 와 b 는 주소가 같지만 a = 20; 으로 값을 변경하면 a는 새로운 주소값을 참조하게 된다. 그렇기 때문에 b 에 아무런 영향을 주지 않는다.
때문에 결론적으로 파이썬에서 얕은 복사냐 깊은 복사냐를 공부해야 하는 객체는 list, set, dictionary 와 같은 mutable 한 객체들이다.
얕은 복사
얕은 복사라는 것은 변수를 복사했다고 생각했지만 실제로는 주소를 복사하는 것을 의미한다.
'=' 대입 연산자를 이용한 복사
arr1 = [1, 2, 3]
arr2 = arr1
print("arr1 값: {0}, 주소값: {1}".format(arr1, hex(id(arr1))))
print("arr2 값: {0}, 주소값: {1}".format(arr2, hex(id(arr2))))
arr1.append(4)
print("arr1 값: {0}, 주소값: {1}".format(arr1, hex(id(arr1))))
print("arr2 값: {0}, 주소값: {1}".format(arr2, hex(id(arr2))))
arr1 값: [1, 2, 3], 주소값: 0x1e6ab9d98c0
arr2 값: [1, 2, 3], 주소값: 0x1e6ab9d98c0
arr1 값: [1, 2, 3, 4], 주소값: 0x1e6ab9d98c0
arr2 값: [1, 2, 3, 4], 주소값: 0x1e6ab9d98c0
arr2 리스트에 arr1 리스트를 얕은 복사하게 되면 메모리 주소만 복사했기 때문에 arr1 에 값이 추가되면 arr2 에도 동일하게 적용되는 것을 볼 수 있다. 이렇게 얕은 복사는 값을 변경하면 다른 변수에도 영향을 끼친다.
'=' 대입 연산자를 이용한 복사뿐만 아니라 [:] 슬라이싱과 copy() 메서드를 이용한 복사는 어떨까?
[:] 슬라이싱을 이용한 복사, copy() 메서드를 이용한 복사
arr1 = [1, 2, [10, 20, 30], 3]
arr2 = arr1[:]
print("arr1 값: {0}, 주소값: {1}".format(arr1, hex(id(arr1))))
print("arr2 값: {0}, 주소값: {1}".format(arr2, hex(id(arr2))))
print("arr1.append(4)")
arr1.append(4)
print("arr1 값: {0}, 주소값: {1}".format(arr1, hex(id(arr1))))
print("arr2 값: {0}, 주소값: {1}".format(arr2, hex(id(arr2))))
arr1 값: [1, 2, [10, 20, 30], 3], 주소값: 0x1e6ab9d2280
arr2 값: [1, 2, [10, 20, 30], 3], 주소값: 0x1e6aba209c0
arr1.append(4)
arr1 값: [1, 2, [10, 20, 30], 3, 4], 주소값: 0x1e6ab9d2280
arr2 값: [1, 2, [10, 20, 30], 3], 주소값: 0x1e6aba209c0
arr1 을 [:] 리스트 슬라이싱을 통해서 arr2 에 복사했다. 주소값을 보면 두 리스트가 참조하는 주소가 다르다는 것을 알 수 있다. 그리고 arr1 에 값을 추가하더라도 arr2 에는 아무런 영향을 주지 않는 것을 볼 수 있다. 그렇다면 얕은 복사가 아닌걸까? 리스트내의 리스트는 어떨까?
print("arr1[2] 값: {0}, 주소값: {1}".format(arr1[2], hex(id(arr1[2]))))
print("arr2[2] 값: {0}, 주소값: {1}".format(arr2[2], hex(id(arr2[2]))))
print("arr1[2].append(40)")
arr1[2].append(40)
print("arr1[2] 값: {0}, 주소값: {1}".format(arr1[2], hex(id(arr1[2]))))
print("arr2[2] 값: {0}, 주소값: {1}".format(arr2[2], hex(id(arr2[2]))))
arr1[2] 값: [10, 20, 30], 주소값: 0x1e6ab9d07c0
arr2[2] 값: [10, 20, 30], 주소값: 0x1e6ab9d07c0
arr1[2].append(40)
arr1[2] 값: [10, 20, 30, 40], 주소값: 0x1e6ab9d07c0
arr2[2] 값: [10, 20, 30, 40], 주소값: 0x1e6ab9d07c0
리스트내의 리스트 주소를 보면 동일한 곳을 가리키고 있는 것을 볼 수 있다. arr1[2] 리스트에 값을 추가했을때 arr2[2] 리스트가 변경되는 것도 확인할 수 있다.
슬라이싱을 이용한 복사는 완전한 깊은 복사도 아니고, 완전한 얕은 복사도 아니지만 이것 또한 얕은 복사로 구분된다. 또한, copy() 메서드를 이용한 즉 arr1.copy(); 를 사용해도 동일한 결과가 나온다.
깊은 복사
깊은 복사를 사용하기 위해서는 copy 모듈의 deepcopy 함수를 사용해야 한다. 깊은 복사는 리스트 내부 리스트, 딕셔너리 내부 리스트 등 내부에 있는 객체를 모두 새롭게 만들어 주는 작업을 한다. deepcopy 를 이용한 예제를 보자.
import copy # copy 모듈 불러오기
d1 = {'a': 'HelloWorld', 'b': [1, 2, 3]}
d2 = copy.deepcopy(d1)
print("1. 전체 출력")
print("d1 : {0}, 주소값 : {1}".format(d1, hex(id(d1))))
print("d2 : {0}, 주소값 : {1}".format(d2, hex(id(d2))))
print("\n2. 딕셔너리에 새 key, value 추가")
print("d2['c'] = 'kimchi'")
d2['c'] = 'kimchi'
print("d1 : {0}, 주소값 : {1}".format(d1, hex(id(d1))))
print("d2 : {0}, 주소값 : {1}".format(d2, hex(id(d2))))
print("\n3. 딕셔너리 내부 리스트")
print("d1['b'] : {0}, 주소값 : {1}".format(d1['b'], hex(id(d1['b']))))
print("d2['b'] : {0}, 주소값 : {1}".format(d2['b'], hex(id(d2['b']))))
print("\n4. 딕셔너리 내부 리스트에 값 추가")
print("d1['b'].append('NO')")
d1['b'].append('NO')
print("d1['b'] : {0}, 주소값 : {1}".format(d1['b'], hex(id(d1['b']))))
print("d2['b'] : {0}, 주소값 : {1}".format(d2['b'], hex(id(d2['b']))))
print("\n5. 전체 출력")
print("d1 : {0}, 주소값 : {1}".format(d1, hex(id(d1))))
print("d2 : {0}, 주소값 : {1}".format(d2, hex(id(d2))))
1. 전체 출력
d1 : {'a': 'HelloWorld', 'b': [1, 2, 3]}, 주소값 : 0x1e6abaae4c0
d2 : {'a': 'HelloWorld', 'b': [1, 2, 3]}, 주소값 : 0x1e6aba28d00
2. 딕셔너리에 새 key, value 추가
d2['c'] = 'kimchi'
d1 : {'a': 'HelloWorld', 'b': [1, 2, 3]}, 주소값 : 0x1e6abaae4c0
d2 : {'a': 'HelloWorld', 'b': [1, 2, 3], 'c': 'kimchi'}, 주소값 : 0x1e6aba28d00
3. 딕셔너리 내부 리스트
d1['b'] : [1, 2, 3], 주소값 : 0x1e6abaae780
d2['b'] : [1, 2, 3], 주소값 : 0x1e6abab0fc0
4. 딕셔너리 내부 리스트에 값 추가
d1['b'].append('NO')
d1['b'] : [1, 2, 3, 'NO'], 주소값 : 0x1e6abaae780
d2['b'] : [1, 2, 3], 주소값 : 0x1e6abab0fc0
5. 전체 출력
d1 : {'a': 'HelloWorld', 'b': [1, 2, 3, 'NO']}, 주소값 : 0x1e6abaae4c0
d2 : {'a': 'HelloWorld', 'b': [1, 2, 3], 'c': 'kimchi'}, 주소값 : 0x1e6aba28d00
d1 과 d2 의 주소값이 다르고, 내부 리스트의 주소값이 다른것을 볼 수 있다. 이렇게 복사한 이후부터는 독립적인 상태가 되는것이 깊은 복사이다.
얕은 복사와 깊은 복사
'언어 > Python' 카테고리의 다른 글
[Python] 자료형 - list, tuple, set, dictionary (0) | 2022.12.28 |
---|