얕은 복사

최동혁·2022년 12월 6일
0

Python

목록 보기
2/10

파이썬의 얕은 복사

파이썬 얕은 복사 ([:], copy, copy.copy)


지난 시간에 배운 immutable, mutable 객체를 생각해보면 조금 쉬울 것.

1-1) 얕은 복사 사용 방법 및 특징 (immutable, mutable 객체의 복사)

얕은 복사라는 것은 변수를 복사했다고 생각했지만 실제로는 연결되어있는 것을 의미한다.

좀 더 자세히 이야기하자면,변수를 복사했지만 참조한 곳은 동일하기 때문에 같은 변수를 가리키고 있는 것이다.

그림으로 한번 보자

arr1 = [1,2,3]이고 arr2 = arr1 이렇게 리스트에 '='을 해서 복사(얕은복사)를 했다고 하면 아래와 같은 그림이 된다.

python shallow copy

python shallow copy

우리는 복사를 했다고 생각하지만 사실 복사한 것은 참조(메모리 주소)만 복사한 것이지 실제 객체 전체를 복사한것이 아니다.

그렇기 때문에 여기서 arr1에 append를 통해서 값을 추가하거나 한다면, arr2에도 동일하게 적용되는 것이다. (같은 곳을 참조하기 때문에)

arr1.appned(4)을 해보면 아래와 같은 그림이 된다.

https://blog.kakaocdn.net/dn/bqC6r5/btrhZNJOrow/NlM67mgX4cdUTj51IUAkr1/img.png

같은곳을 가리키기 때문에 arr1에서 4 값이 추가되면, arr2 도 4가 적용이 된다.

이렇게 복사를 했음에도, 값을 변경하면 다른 변수에도 영향을 끼치도록 '참조'만 복사한 것을 얕은 복사라고 한다.

immutable 한 객체들 int, float 등은 얕은 복사를 하던 깊은 복사를 하던 사실 상관이 없다.

왜냐하면 해당 객체들은 값이 변경되면 '무조건' 참조가 변경되기 때문에얕은 복사를 해서 값을 변경하더라도, 참조하던 다른 객체의 값도 변경되거나 하지 않기 때문이다.

immutable 객체인 int 타입으로 예시를 한번 들어보자.

아래 그림을 보면 num1 = 3num2 = num1 을 하게 되면 메모리 상에서 그림이 이렇게 될 것이다.

num1, num2 가 3이라는 값을 가진 메모리 공간을 같이 참조한다.

python copy

python copy

이때 num1 = 4를 하게 되면, immutable 객체는 값이 변경될 수 없기 때문에 새롭게 메모리를 할당해서 4라는 값을 생성하고 그곳을 num1 이 참조하게 한다.

그럼 num1, num2는 다른 곳을 가리키게 된다.

python copy

python copy

결론적으로 파이썬에서는 "얕은 복사"냐 "깊은 복사"냐에 대해서 구분하고 학습해야 하는 객체는 int, float와 같은 immutable 한 객체들이 아니라 list, set, dictionary와 같은 mutable 한 객체들이다.

그럼 이제 얕은 복사를 하는 방법들에 대해서 알아보자.

mutable 객체의 얕은 복사를 하는 방법은 4가지이다.

1-2) '=' 대입 연산자를 이용한 얕은 복사

위에서 설명한 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

python = copy

  • mutable 한 객체인 리스트 예제 arr1, arr2를 '='을 통해서 복사를 하고 값과 주소를 보면 동일한 곳을 가리키고 있는걸 알 수 있다.
  • 여기서 arr2.append(99)를 통해서 arr2에 값을 추가한 후에 arr1, arr2 를 둘 다 출력을 해보면 둘 다 [1,2,3]에서 [1,2,3,99]로 값이 변경된 것을 알 수 있고, 참조하는 주소 또한 동일한 것을 알 수 있다.

이것이 참조만 복사하는 얕은 복사이다.

  • immutable 한 객체 int 예제
    • int 타입을 복사하면 같은 참조를 가리키게 되고, 값을 변경했을 때 다른 주소를 가리키게 되는 것을 볼 수 있다. 결국 각개 다른 참조.

1-3) [:] 슬라이싱을 이용한 얕은 복사. (feat 눈속임)

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

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] 리스트를 가리키고 있던 것이었다.

완전한 깊은 복사도 아니고, 완전한 얕은 복사도 아니다. 그렇지만 이것 또한 얕은 복사로 구분한다.

1-4) copy 메서드 이용 (객체에 제공)한 얕은 복사

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()

python list.copy()

1-5) copy 모듈의 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

python copy.copy dictionary

위 리스트 예제와 동일하게 dictionary 도 복사를 했을 때 d1, d2 객체의 주소가 달라서 깊은 복사처럼 보인다.

특히 d2['c'] = 'kimchi'를 통해서 d2 에만 key, value를 추가가 되는 것을 보면 정말 깊은 복사처럼 보이긴 한다만,

d1['b'], d2['b'] 의 value 값인 리스트 [1,2,3]을 보면 주소가 동일한 것을 볼 수 있다.

결과에서 보듯이 해당 리스트에 값을 추가하면 d1, d2에 둘 다 추가된 것을 볼 수 있다.

profile
항상 성장하는 개발자 최동혁입니다.

0개의 댓글