지난 시간에 배운 immutable, mutable 객체를 생각해보면 조금 쉬울 것.
얕은 복사라는 것은 변수를 복사했다고 생각했지만 실제로는 연결되어있는 것을 의미한다.
좀 더 자세히 이야기하자면,변수를 복사했지만 참조한 곳은 동일하기 때문에 같은 변수를 가리키고 있는 것이다.
그림으로 한번 보자
arr1 = [1,2,3]이고 arr2 = arr1 이렇게 리스트에 '='을 해서 복사(얕은복사)를 했다고 하면 아래와 같은 그림이 된다.
python shallow copy
우리는 복사를 했다고 생각하지만 사실 복사한 것은 참조(메모리 주소)만 복사한 것이지 실제 객체 전체를 복사한것이 아니다.
그렇기 때문에 여기서 arr1에 append를 통해서 값을 추가하거나 한다면, arr2에도 동일하게 적용되는 것이다. (같은 곳을 참조하기 때문에)
arr1.appned(4)을 해보면 아래와 같은 그림이 된다.
같은곳을 가리키기 때문에 arr1에서 4 값이 추가되면, arr2 도 4가 적용이 된다.
이렇게 복사를 했음에도, 값을 변경하면 다른 변수에도 영향을 끼치도록 '참조'만 복사한 것을 얕은 복사라고 한다.
immutable 한 객체들 int, float 등은 얕은 복사를 하던 깊은 복사를 하던 사실 상관이 없다.
왜냐하면 해당 객체들은 값이 변경되면 '무조건' 참조가 변경되기 때문에얕은 복사를 해서 값을 변경하더라도, 참조하던 다른 객체의 값도 변경되거나 하지 않기 때문이다.
immutable 객체인 int 타입으로 예시를 한번 들어보자.
아래 그림을 보면 num1 = 3num2 = num1 을 하게 되면 메모리 상에서 그림이 이렇게 될 것이다.
num1, num2 가 3이라는 값을 가진 메모리 공간을 같이 참조한다.
python copy
이때 num1 = 4를 하게 되면, immutable 객체는 값이 변경될 수 없기 때문에 새롭게 메모리를 할당해서 4라는 값을 생성하고 그곳을 num1 이 참조하게 한다.
그럼 num1, num2는 다른 곳을 가리키게 된다.
python copy
결론적으로 파이썬에서는 "얕은 복사"냐 "깊은 복사"냐에 대해서 구분하고 학습해야 하는 객체는 int, float와 같은 immutable 한 객체들이 아니라 list, set, dictionary와 같은 mutable 한 객체들이다.
그럼 이제 얕은 복사를 하는 방법들에 대해서 알아보자.
mutable 객체의 얕은 복사를 하는 방법은 4가지이다.
위에서 설명한 int, list를 비교해 보면서 보면 좋다.
이번 예제까지만 immutable 타입인 int 타입 예제를 들고 아래에서부터는 mutable 타입만 예제로 쓰겠다.
(어차피 immutable 타입은 깊은, 얕은 복사 구분이 상관없기 때문)
# mutable 한 객체 (리스트)
print('=' * 50)
arr1 = [1, 2, 3]
arr2 = arr1 # '=' 복사
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
arr2.append(99) # arr2 에 값 추가
print('\narr2.append(99)')
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
# immutable 한 객체 (int)
print('=' * 50)
num1 = 30
num2 = num1 # 복사
print(f'num1 : {num1}, add : {hex(id(num1))}')
print(f'num2 : {num2}, add : {hex(id(num2))}')
num2 += 1
print('\nnum2 += 1')
print(f'num1 : {num1}, add : {hex(id(num1))}')
print(f'num2 : {num2}, add : {hex(id(num2))}')
python = copy
이것이 참조만 복사하는 얕은 복사이다.
print('=' * 50)
arr1 = [4, 5, 6, [2, 4, 8]]
arr2 = arr1[:] # 여기서 복사
print("1. 전체 출력")
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
print("\n2. 리스트의 끝에 값 추가")
arr2.append(22) # arr2 에 값 추가
print('arr2.append(22)')
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
# 리스트 안에 있는 리스트
print("\n3. 리스트 내부 리스트")
print(f'arr1[3] : {arr1[3]}, add : {hex(id(arr1[3]))}')
print(f'arr2[3] : {arr2[3]}, add : {hex(id(arr2[3]))}')
print("\n4. 리스트 내부 리스트에 값 추가")
arr1[3].append(99)
print('arr1[3].append(99)')
print(f'arr1[3] : {arr1[3]}, add : {hex(id(arr1[3]))}')
print(f'arr2[3] : {arr2[3]}, add : {hex(id(arr2[3]))}')
print("\n5. 리스트 전체 다시 확인")
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
python [:] copy
1. 전체 출력
arr1을 [:] 리스트 슬라이싱을 통해서 arr2에 복사를 했다.
전체 출력 부분을 보면 보면 arr1과 arr2가 참조하는 메모리 주소가 다른 것을 볼 수 있다.
그래서 딱 봤을 때. "어? 메모리 주소 다르니까 깊은 복사 아니냐" 할 수 있다.
2. 리스트 끝에 값 추가
그래서 arr2.append(22) 를 통해서 리스트 끝에 값을 추가해보았다.
그럼 arr1 = [4, 5, 6, [2, 4, 8]] 이 되고 arr2 = [4, 5, 6, [2, 4, 8], 22]로 리스트의 값이 다른 것을 볼 수 있다.
이렇게만 보면 깊은 복사인 것 같은데.. 왜 얕은 복사라고 하는지???
3. 리스트 내부 리스트
"리스트 안에 존재하는 리스트" 이 부분을 보면 확실히 얕은 복사인 게 느껴진다.
arr1 = [4, 5, 6, [2, 4, 8]]
arr2 = [4, 5, 6, [2, 4, 8], 22]
arr1[3] 과 arr2[3]이 바로 저 [2, 4, 8] 리스트이다.
이 부분의 주소를 출력해보면 두 내부 리스트가 동일한 곳을 가리키고 있는 것을 볼 수 있다.
'아 이런 깊은 것 같았지만... 얕은 복사네..'
4. 리스트 내부 리스트 값 추가
그럼 arr1[3] 부분이 정말 얕은 복사가 된 게 맞나 값을 추가해자.
arr1[3].append(99) 를 추가해서 출력해보니 arr1[3] 은 [2,4,8, 99]가 되었고 arr2[3] 또한 [2,4,8,99]가 된 것을 볼 수 있다.
야속한 얕은 복사…
5. 전체 출력을 다시 한번 해보면
arr1 = [4, 5, 6, [2, 4, 8, 99]]
arr1 = [4, 5, 6, [2, 4, 8, 99], 22]
역시나 깊은 복사인 줄 알았던 [:] 슬라이싱이 내부적으로 보면 얕은 복사이었던 것을 알 수 있다.
겉에 있는 리스트만 새롭게 객체를 추가했지만 사실 내부에 있는 리스트 요소는 하나의 [2,4,8] 리스트를 가리키고 있던 것이었다.
copy 메서드, copy 함수를 이용해도 [:]와 동일한 결과가 나온다.
설명은 위 1-3) [:] 복사와 동일하다.
코드와 결과만 첨부.
print('=' * 50)
arr1 = [4, 5, 6, [2, 4, 8]]
arr2 = arr1.copy() # 여기서 복사 copy 메소드 이용
print("1. 전체 출력")
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
print("\n2. 리스트의 끝에 값 추가")
arr2.append(22) # arr2 에 값 추가
print('arr2.append(22)')
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
# 리스트 안에 있는 리스트
print("\n3. 리스트 내부 리스트")
print(f'arr1[3] : {arr1[3]}, add : {hex(id(arr1[3]))}')
print(f'arr2[3] : {arr2[3]}, add : {hex(id(arr2[3]))}')
print("\n4. 리스트 내부 리스트에 값 추가")
arr1[3].append(99)
print('arr1[3].append(99)')
print(f'arr1[3] : {arr1[3]}, add : {hex(id(arr1[3]))}')
print(f'arr2[3] : {arr2[3]}, add : {hex(id(arr2[3]))}')
print("\n5. 리스트 전체 다시 확인")
print(f'arr1 : {arr1}, add : {hex(id(arr1))}')
print(f'arr2 : {arr2}, add : {hex(id(arr2))}')
python list.copy()
copy 모듈의 copy 함수를 이용해도 얕은 복사를 할 수 있다.
import copy를 작성해주어야 한다.
이번엔 리스트 말고 딕셔너리를 이용해자.
import copy # copy 모듈 불러오기
print('=' * 50)
d1 = {'a': 'BlockDMask', 'b': [1, 2, 3]}
d2 = copy.copy(d1) # copy 모듈 얕은복사
print("1. 전체 출력")
print(f'd1 : {d1}, address : {hex(id(d1))}')
print(f'd2 : {d2}, address : {hex(id(d2))}')
print("\n2. 딕셔너리에 새 key, value 추가")
d2['c'] = 'kimchi'
print("d2['c'] = 'kimchi'")
print(f'd1 : {d1}, address : {hex(id(d1))}')
print(f'd2 : {d2}, address : {hex(id(d2))}')
# 딕셔너리 내부에 리스트 value
print("\n3. 딕셔너리 내부 리스트")
print(f"d1['b'] : {d1['b']}, address : {hex(id(d1['b']))}")
print(f"d2['b'] : {d2['b']}, address : {hex(id(d2['b']))}")
print("\n4. 딕셔너리 내부 리스트에 값 추가")
d1['b'].append('NO')
print("d1['b'].append('NO')")
print(f"d1['b'] : {d1['b']}, address : {hex(id(d1['b']))}")
print(f"d2['b'] : {d2['b']}, address : {hex(id(d2['b']))}")
print("\n5. 딕셔너리 전체 다시 확인")
print(f'd1 : {d1}, address : {hex(id(d1))}')
print(f'd2 : {d2}, address : {hex(id(d2))}')
python copy.copy dictionary
위 리스트 예제와 동일하게 dictionary 도 복사를 했을 때 d1, d2 객체의 주소가 달라서 깊은 복사처럼 보인다.
특히 d2['c'] = 'kimchi'를 통해서 d2 에만 key, value를 추가가 되는 것을 보면 정말 깊은 복사처럼 보이긴 한다만,
d1['b'], d2['b'] 의 value 값인 리스트 [1,2,3]을 보면 주소가 동일한 것을 볼 수 있다.
결과에서 보듯이 해당 리스트에 값을 추가하면 d1, d2에 둘 다 추가된 것을 볼 수 있다.