A1: 깊은복사와 얕은복사

Author

최규빈

Published

December 7, 2022

모르고 살았어도 좋았을 내용

Introduction

비상식적인 append

- 아래의 코드를 관찰하자.

a=[1,2,3]
b=a
a=a+[4]

현재 a,b의 출력결과는?

print('a=',a)
print('b=',b)
a= [1, 2, 3, 4]
b= [1, 2, 3]
  • 상식적인 결과

- 이제 다시 아래의 코드를 관찰하자.

a=[1,2,3]
b=a
a.append(4) 

현재 a,b의 출력결과는?

print('a=',a)
print('b=',b)
a= [1, 2, 3, 4]
b= [1, 2, 3, 4]
  • 이상한결과. a만 바꿨는데 왜 b도 같이 바뀌는거야??

append의 동작원리: 틀린상상

- 아래의 코드를 다시 살펴보자.

a=[1,2,3]
b=a
a.append(4)

a,b라는 변수들은 메모리에 어떻게 저장이 되어있을까?

상상력을 조금 발휘하면 아래와 같이 여길 수 있다.

  1. 메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.

  2. 아래를 실행하였을 경우

a=[1,2,3]
  • 메모리주소1에 존재하는 방을 a라고 하고, 그 방에 [1,2,3]을 넣는다.
  1. 아래를 실행하였을 경우
b=a
  • 메모리주소2에 존재하는 방을 b라고 하고, 그 방에 a를 넣어야하는데, a는 [1,2,3]이니까 [1,2,3]을 넣는다.
  1. 아래를 실행하면
a.append(4)
  • 방 a로가서 [1,2,3]을 [1,2,3,4]로 바꾼다.
  • 그리고 방 b에는 아무것도 하지 않는다.

- R에서는 맞는 비유인데, 파이썬은 적절하지 않은 비유이다.

틀린이유

id(a)
139753545242336
id(b)
139753545242336

실제로는 a,b가 저장된 메모리 주소가 동일함

append의 동작원리: 올바른 상상

- 파이썬에서는 아래가 더 적절한 비유이다.

  1. 메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.

  2. 아래를 실행하였을 경우

a=[1,2,3]
  • 메모리주소 139753545242336에서 [1,2,3]을 생성
  • 139753545242336의 방문에 a라는 포스트잇을 붙인다.
  • 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서 a라는 포스트잇이 붙은 방을 찾아가면 된다.
  1. 아래를 실행하였을 경우
b=a
  • a라는 포스트잇이 있는데, a라는 포스트잇이랑 b라는 포스트잇과 같은 효과를 주도록 한다.
  • 쉽게말하면 b라는 포스트잇을 방 139753545242336의 방문에 붙인다는 이야기.
  • 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서 a라는 포스트잇이 붙어 있거나 b라는 포스트잇이 붙어있는 방을 찾아가면 된다.
  1. 아래를 실행하면
a.append(4)
  • a라는 포스트잇이 붙어있는 방으로 가서, 그 내용물에 append함수를 적용하여 4를 추가하라. 즉 내용물 [1,2,3]을 [1,2,3,4]로 바꾸라.
  • 같은방(139753545242336)에 a,b라는 포스트잇이 모두 붙어있음. 따라서 b라는 포스트잇이 붙은 방을 찾아가서 내용물을 열어보면 [1,2,3,4]가 나온다.

할당문(=)의 이해

- 파이썬에서 할당문을 이해하기 위해서는 언제나 오른쪽을 먼저 읽어야 한다.

  • 할당문의 오른쪽에서는 객체를 생성하거나 가져옴
  • 그 후에 라벨을 붙이듯이 할당문 왼쪽의 변수가 할당문 오른쪽의 객체에 바인딩 된다. (참조)

- b=a

나는 이미 a가 의미하는게 무엇인지 알고있어. 그런데 그 실체를 b라고도 부르고 싶어.

라는 것과 같다. 즉 이미 a라고 부르고 있는것을 내가 b라고도 부르고 싶다는 의미인데 이는 마치 별명과 같다. (ba의 별명, alias) 그리고 이처럼 하나의 오브젝트에 여러개의 이름을 붙이는 것을 에일리어싱이라고 부른다.

id, value

예제1

a=[1,2,3]
b=a
a.append(4)
c=[1,2,3,4]

여기에서 a,b,c는 모두 같은 value를 가진다.

a
[1, 2, 3, 4]
b
[1, 2, 3, 4]
c
[1, 2, 3, 4]

하지만 그 id까지 같은 것은 아니다.

id(a), id(b), id(c)
(139851739924096, 139851739924096, 139851742724800)

예제2

(관찰)

a=[1,2,3] 
b=a 
a=[1,2,3]+[4] 
print('a=',a)
print('b=',b)
a= [1, 2, 3, 4]
b= [1, 2, 3]

(해설)

id(a),id(b)
(140346713595728, 140346713595168)
  • 포인트: [1,2,3]+[4] 가 실행되는 순간 새로운 오브젝트가 만들어지고 그 오브젝트를 a라는 이름으로 다시 할당되었음.

인터닝

예제1

a=1+2021
id(a)
139753546122608
b=2023-1
id(b)
139753545299280
id(2022)
139753545299472
  • 당연한결과임.

예제2: 이제 다 이해했다고 생각했는데..

a=1+2 
id(a)
7394720
b=4-1
id(b)
7394720
  • id(a)와 id(b)가 왜 똑같지..?

(해설) 파이썬의 경우 효율성을 위해서 -5~256까지의 정수를 미리 저장해둠.

id(3)
7394720
  • 3은 언제나 7394720에 지박령마냥 밖혀있음

.copy()의 사용 (shallow copy의 사용)

예제1

(관찰) 아래의 예제를 살펴보자. 참조를 제대로 이해했다면 아래의 예제는 자연스럽게 이해가능할 것임.

l1 = [3, [66,55,44]]
l2 = l1 
print('시점1')
print('l1=',l1)
print('l2=',l2)

l1[0]=4 
print('시점2')
print('l1=',l1)
print('l2=',l2)

l2.append(5)
print('시점3')
print('l1=',l1)
print('l2=',l2)
시점1
l1= [3, [66, 55, 44]]
l2= [3, [66, 55, 44]]
시점2
l1= [4, [66, 55, 44]]
l2= [4, [66, 55, 44]]
시점3
l1= [4, [66, 55, 44], 5]
l2= [4, [66, 55, 44], 5]

(해설)

l1 = [3, [66,55,44]]
l2 = l1 
id(l1),id(l2)
(139753545268832, 139753545268832)

예제2: R과 같이 = 를 쓰고 싶다면?

(관찰)

l1 = [3, [66,55,44]]
l2 = l1.copy()
print('시점1')
print('l1=',l1)
print('l2=',l2)

l1[0]=4 
print('시점2')
print('l1=',l1)
print('l2=',l2)

l2.append(5)
print('시점3')
print('l1=',l1)
print('l2=',l2)
시점1
l1= [3, [66, 55, 44]]
l2= [3, [66, 55, 44]]
시점2
l1= [4, [66, 55, 44]]
l2= [3, [66, 55, 44]]
시점3
l1= [4, [66, 55, 44]]
l2= [3, [66, 55, 44], 5]

(해설)

l1 = [3, [66,55,44]]
l2 = l1.copy()
id(l1),id(l2) ## 드디어 주소가 달라졌다.
(140346713602720, 140346713599104)

예제3: 이제 다 이해했다고 생각했는데..

(관찰)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1[1].append(33)
print('l1=',l1)
print('l2=',l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]

(의문)

id(l1),id(l2)
(140346713608432, 140346731755152)
  • l1이랑 l2의 주소도 다르게 나오는데 왜 또 참조한것마냥 l1과 l2가 같이 바뀌고 있지?

Shallow copy의 이해

- 방금 살펴본 예제3을 이해하기 위해서는 shallow copy를 이해해야 한다.

예제1

(관찰+해설)

a=2222
b=2222
id(a),id(b)
(139753545300880, 139753545301808)

메모리 상황

  1. 2222라는 오브젝트가 어떤공간(139753545300880)에 생성되고 그 공간에 a라는 라벨이 붙음
  2. 2222라는 오브젝트가 어떤공간(139753545301808)에 생성되고 그 공간에 b라는 라벨이 붙음

즉 -5~256 이외의 2개의 메모리 공간을 추가적으로 사용

예제2

(관찰)

a=[1,2,2222]
b=[1,2,2222]
a.append(4)
print('a=',a)
print('b=',b)
a= [1, 2, 2222, 4]
b= [1, 2, 2222]

(해설)

a=[1,2,2222]
b=[1,2,2222]
id(a), [id(a[0]),id(a[1]),id(a[2])] # a=[1,2,2222]
(139753182327904, [7394656, 7394688, 139753178093776])
id(b), [id(b[0]),id(b[1]),id(b[2])] # b=[1,2,2222] 
(139753173818656, [7394656, 7394688, 139753178095568])
a.append(4)
a
[1, 2, 2222, 4]
b
[1, 2, 2222]

메모리상황

  1. -5~256까지의 숫자는 미리 메모리에 저장되어 있다. 이중에서 1은 7394656, 2는 7394688에 저장되어있음.
  2. 2222가 공간 139753178093776에서 만들어진다.
  3. 어떠한 리스트오브젝트가 공간 139753182327904에서 만들어지고 원소로 [1,2,2222]를 가진다. 이 공간에 a라는 포스트잇을 붙인다.
  4. 2222가 공간 139753178095568에서 만들어진다.
  5. 어떠한 리스트오브젝트가 공간 139753173818656에서 만들어지고 원소로 [1,2,2222]를 가진다. 이 공간에 b라는 포스트잇을 붙인다.
  6. a라는 포스트잇이 붙은 공간으로 이동하여 원소에 4를 추가시킨다.

즉 -5~256이외에 4개의 메모리 공간을 추가사용 (a,b,a의 2222,b의 2222)

예제3

(관찰)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1[0] = 7777
print('l1=',l1)
print('l2=',l2)
l1= [7777, [66, 55, 44]]
l2= [3, [66, 55, 44]]

(해설)

l1 = [3,[66,55,44]]
l2 = l1.copy()
id(l1), [id(l1[0]), id(l1[1])]
(139753183437040, [7394720, 139753183707216])
id(l2), [id(l2[0]), id(l2[1])]
(139753182311120, [7394720, 139753183707216])

메모리상황

  1. -5~256까지의 숫자가 메모리에 저장되어 있다.
  2. 저장된 숫자중 66,55,44를 묶어서 리스트로 구성하고 이 리스트를 공간 139753183707216에 저장.
  3. 숫자 3과 공간 139753183707216에 저장된 리스트 [66,55,44]를 하나로 묶어서 새로운 리스트를 구성하고 이를 공간 139753183437040에 저장. 공간 139753183437040l1이라는 포스트잇 생성.
  4. 공간 139753182311120l1의 원소들을 모아서 새로운 리스트를 구성함. 공간 139753182311120l2라는 포스트잇 생성.
l1[0] = 7777
l1,l2
([7777, [66, 55, 44]], [3, [66, 55, 44]])
id(l1), [id(l1[0]), id(l1[1])]
(139753183437040, [139753178092080, 139753183707216])
id(l2), [id(l2[0]), id(l2[1])]
(139753182311120, [7394720, 139753183707216])
  1. l1[0]은 원래 공간 7394720와 binding 되어 있었음.
  2. 그런데 7777이라는 새로운 오브젝트가 공간 139753178092080에 생성되고 l1[0]이 공간 139753178092080와 다시 binding 됨.

예제4

(관찰)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1.append(7777)
print('l1=',l1)
print('l2=',l2)
l1= [3, [66, 55, 44], 7777]
l2= [3, [66, 55, 44]]

(해설)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1.append(7777)
l1,l2
([3, [66, 55, 44], 7777], [3, [66, 55, 44]])
id(l1), [id(l1[0]), id(l1[1]), id(l1[2])]
(139753183257056, [7394720, 139753184484240, 139753180268560])
id(l2), [id(l2[0]), id(l2[1])]
(139753183216656, [7394720, 139753184484240])

예제3, 예제4를 통하여 리스트가 가변형객체라는 것을 확인할 수 있다. 예제3의 경우 l1이 저장되어있던 메모리공간의 내용물이 [3,[66,55,44]] 에서 [7777,[66,55,44]] 로 바뀌었다. 예제4의 경우 l1이 저장되어있던 메모리공간의 내용물이 [3,[66,55,44]] 에서 [3,[66,55,44],7777] 로 바뀌었다.

예제5: 우리를 힘들게 했던 그 예제.

(관찰)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1[1].append(7777)
print('l1=',l1)
print('l2=',l2)
l1= [3, [66, 55, 44, 7777]]
l2= [3, [66, 55, 44, 7777]]

(해설-시점1)

l1 = [3,[66,55,44]]
l2 = l1.copy()
l1,l2
([3, [66, 55, 44]], [3, [66, 55, 44]])
id(l1), [id(l1[0]), id(l1[1])]
(139753181411920, [7394720, 139753181409920])
id(l2), [id(l2[0]), id(l2[1])]
(139753181409440, [7394720, 139753181409920])

(해설-시점2)

l1[1].append(7777)
l1,l2
([3, [66, 55, 44, 7777]], [3, [66, 55, 44, 7777]])
id(l1), [id(l1[0]), id(l1[1])]
(139753181411920, [7394720, 139753181409920])
id(l2), [id(l2[0]), id(l2[1])]
(139753181409440, [7394720, 139753181409920])

해설: 사실 시점1에서 메모리 주소상황을 잘 이해했다면 신기한 일이 아니다. .copy()는 l1과 l2의 주소만 다르게 만들 뿐 내용물인 l1[0],l1[1]는 동일하니까.

예제6: 신임교수=[‘최규빈’,‘이영미’]

- 최규빈, 이영미는 신임교수임

신임교수 = ['최규빈','이영미']
id(신임교수), id('최규빈'), id('이영미')
(139753182527808, 139753171447312, 139753171447408)

- 신임교수를 누군가는 막내들이라고 부르기도 함.

막내들 = 신임교수 
id(막내들), id(신임교수)
(139753182527808, 139753182527808)

“막내들”이라는 단어와 “신임교수”라는 단어는 사실 같은 말임

- 새로운 교수 “박혜원”이 뽑혔음.

신임교수.append("박혜원")
신임교수, 막내들
(['최규빈', '이영미', '박혜원'], ['최규빈', '이영미', '박혜원'])

- 전북대 통계학과에서 R특강팀을 구성하여 방학중 R교육을 실시하고자함. 특강팀은 우선 신임교수들로 구성.

R특강팀 = 신임교수.copy()
R특강팀 
['최규빈', '이영미', '박혜원']

- R특강팀에 최혜미교수님 추가. (그렇지만 최혜미교수님이 막내는 아니야.. // 참조와 shallow copy의 차이점)

R특강팀.append("최혜미") 
R특강팀, 신임교수, 막내들
(['최규빈', '이영미', '박혜원', '최혜미'], ['최규빈', '이영미', '박혜원'], ['최규빈', '이영미', '박혜원'])

- R특강팀에서 양성준 교수를 추가하여 파이썬 특강팀을 구성

파이썬특강팀 = [R특강팀, "양성준"]
파이썬특강팀
[['최규빈', '이영미', '박혜원', '최혜미'], '양성준']

- 이영미교수는 다른 일이 많아서 R특강 팀에서 제외됨. (그럼 자연히 파이썬에서도 제외됨!!)

R특강팀.remove("이영미")
R특강팀, 파이썬특강팀
(['최규빈', '박혜원', '최혜미'], [['최규빈', '박혜원', '최혜미'], '양성준'])

하지만 이영미교수는 여전히 신임교수이면서 막내들임

신임교수, 막내들
(['최규빈', '이영미', '박혜원'], ['최규빈', '이영미', '박혜원'])

- 새로운 교수로 “손흥민”이 임용됨.

막내들.append("손흥민")
막내들, 신임교수
(['최규빈', '이영미', '박혜원', '손흥민'], ['최규빈', '이영미', '박혜원', '손흥민'])

- 그렇다고 해서 손흥민 교수가 바로 R이나 파이썬 특강팀에 자동소속되는건 아님

R특강팀, 파이썬특강팀
(['최규빈', '박혜원', '최혜미'], [['최규빈', '박혜원', '최혜미'], '양성준'])

Deep copy

예제1: Motivation example

- 아래의 상황을 다시 생각해보자.

파이썬특강팀 = ["양성준",["최규빈","이영미","최혜미"]]
ADSP특강팀 = 파이썬특강팀.copy()
파이썬특강팀[-1].remove("이영미")
파이썬특강팀, ADSP특강팀
(['양성준', ['최규빈', '최혜미']], ['양성준', ['최규빈', '최혜미']])

이슈: 이영미교수가 파이썬특강에서 제외되면서 ADSP특강팀에서도 제외되었음. 그런데 사실 이영미교수가 파이썬특강팀에서만 제외되길 원한 것이지 ADSP특강팀에서 제외되길 원한게 아닐수도 있음.

해결: Deep copy의 사용

import copy
파이썬특강팀 = ["양성준",["최규빈","이영미","최혜미"]]
ADSP특강팀 = copy.deepcopy(파이썬특강팀)
파이썬특강팀[-1].remove("이영미")
파이썬특강팀, ADSP특강팀
(['양성준', ['최규빈', '최혜미']], ['양성준', ['최규빈', '이영미', '최혜미']])

예제2

- deepcopy

l1 = [3,[66,[55,44]]] 
l2 = copy.deepcopy(l1)
l2[1][1].append(33)
l1,l2
([3, [66, [55, 44]]], [3, [66, [55, 44, 33]]])
print('level 1')
print('l1:', id(l1))
print('l2:', id(l2))
level 1
l1: 140346731797872
l2: 140346713502576
  • 레벨1: l1,l2 의 메모리 주소가 다름을 확인
print('level 2')
print('l1:', id(l1), [id(l1[0]),id(l1[1])])
print('l2:', id(l2), [id(l2[0]),id(l2[1])])
level 2
l1: 140346731797872 [7394720, 140346713544496]
l2: 140346713502576 [7394720, 140346478134928]
  • 레벨2: l1안에 있는 [66,[55,44]]l2안에 있는 [66,[55,44]]의 메모리 주소가 다름도 확인.
print('level 3')
print('l1:', id(l1), [id(l1[0]),[id(l1[1][0]),id(l1[1][1])]])
print('l2:', id(l2), [id(l2[0]),[id(l2[1][0]),id(l2[1][1])]])
level 3
l1: 140346731797872 [7394720, [7396736, 140346713594848]]
l2: 140346713502576 [7394720, [7396736, 140346477770704]]
  • 레벨3: l1안의 [66,[55,44]] 안의 [55,44]l2안의 [66,[55,44]] 안의 [55,44]의 메모리 주소까지도 다름을 확인.

- 비교를 위한 shallow copy

l1 = [3,[66,[55,44]]] 
l2 = l1.copy()
l2[1][1].append(33)
l1,l2
([3, [66, [55, 44, 33]]], [3, [66, [55, 44, 33]]])
print('level 1')
print('l1:', id(l1))
print('l2:', id(l2))
level 1
l1: 140346478137008
l2: 140346477791984
  • 레벨1: l1,l2 의 메모리 주소가 다름을 확인
print('level 2')
print('l1:', id(l1), [id(l1[0]),id(l1[1])])
print('l2:', id(l2), [id(l2[0]),id(l2[1])])
level 2
l1: 140346713603280 [7394720, 140346713602720]
l2: 140346713602880 [7394720, 140346713602720]
  • 레벨2: l1안에 있는 [66,[55,44]]l2안에 있는 [66,[55,44]]의 메모리 주소는 같음!!
print('level 3')
print('l1:', id(l1), [id(l1[0]),[id(l1[1][0]),id(l1[1][1])]])
print('l2:', id(l2), [id(l2[0]),[id(l2[1][0]),id(l2[1][1])]])
level 3
l1: 140346713603280 [7394720, [7396736, 140346713556624]]
l2: 140346713602880 [7394720, [7396736, 140346713556624]]
  • 레벨3: l1안의 [66,[55,44]] 안의 [55,44]l2안의 [66,[55,44]] 안의 [55,44]의 메모리 주소도 같음!!

- 비교를 위한 참조

l1 = [3,[66,[55,44]]] 
l2 = l1
l2[1][1].append(33)
l1,l2
([3, [66, [55, 44, 33]]], [3, [66, [55, 44, 33]]])
print('level 1')
print('l1:', id(l1))
print('l2:', id(l2))
level 1
l1: 140346478134288
l2: 140346478134288
  • 레벨1: l1,l2 여기서부터 메모리 주소가 같다.
print('level 2')
print('l1:', id(l1), [id(l1[0]),id(l1[1])])
print('l2:', id(l2), [id(l2[0]),id(l2[1])])
level 2
l1: 140346478134288 [7394720, 140346713615648]
l2: 140346478134288 [7394720, 140346713615648]
print('level 3')
print('l1:', id(l1), [id(l1[0]),[id(l1[1][0]),id(l1[1][1])]])
print('l2:', id(l2), [id(l2[0]),[id(l2[1][0]),id(l2[1][1])]])
level 3
l1: 140346478134288 [7394720, [7396736, 140346713786480]]
l2: 140346478134288 [7394720, [7396736, 140346713786480]]

문헌에 따라서 shallow copy를 레벨1 deep copy라고 부르기도 한다.

Shallow copy 연습문제

예제1

- 아래의 코드결과를 예측하라. 결과가 나오는 이유를 설명하라.

l1= [3,[66,55,44]]
l2= l1.copy() 
l1[-1].append(33)
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]
  • 포인트: shallow copy 이므로 l1안의 [66,55,44]l2안의 [66,55,44]는 같은 메모리 주소를 가짐

예제2

- 아래의 코드결과를 예측하라. 결과가 나오는 이유를 설명하라.

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] = l1[-1]+[33] 
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44]]
  • 포인트: l1[-1]+[33]가 실행되는 순간 새로운 오브젝트가 생성되고 이 새로운 오브젝트가 l1의 마지막 원소에 새롭게 할당된다.

예제3

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] = l1[-1]+[33] 
l1[-1].remove(33)
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44]]
l2= [3, [66, 55, 44]]
  • 포인트: 이 상황에서 l1안의 [66,55,44]l2안의 [66,55,44]는 서로 다른 메모리 주소를 가진다.

예제4

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] = l1[-1]+[33] 
l1[-1].remove(33)
l1[-1].append(33)

(잘못된 상상) 아래의 코드와 결과가 같을거야!!

l1= [3,[66,55,44]] 
l2= l1.copy() 
# l1[-1] = l1[-1]+[33] 
# l1[-1].remove(33)
l1[-1].append(33)
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]

(하지만 현실은)

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] = l1[-1]+[33] 
l1[-1].remove(33)
l1[-1].append(33)
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44]]
  • 포인트: 예제3을 이해했다면 그냥 이해되는것

예제5

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] += [33] # l1[-1] = l1[-1]+[33] 
l1[-1].remove(33)
l1[-1].append(33)
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]
  • 포인트: += 연산자의 올바른 이해

??? 예제4랑 예제5는 같은코드가 아니었음!!! a += [1] 는 새로운 오브젝트를 만드는게 아니고, 기존의 오브젝트를 변형하는 스타일의 코드였음! (마치 append 메소드처럼)

불변형 객체

Motivation example

- 우리는 이제 아래의 내용은 마스터함

l1= [3,[66,55,44]] 
l2= l1.copy() 
l1[-1] += [33] # l1[-1].append(33)이랑 같은거..
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]

- 아래의 결과를 한번 예측해볼까?

l1=[3,(66,55,44)]
l2=l1.copy()
l2[1] += (33,)
print('l1=', l1)
print('l2=', l2)
l1= [3, (66, 55, 44)]
l2= [3, (66, 55, 44, 33)]

해설

(시점1)

l1=[3,(66,55,44)]
l2=l1.copy()
l1,l2
([3, (66, 55, 44)], [3, (66, 55, 44)])
print('level 1')
print('l1:', id(l1))
print('l2:', id(l2))
level 1
l1: 139753183621520
l2: 139753181521472
print('level 2')
print('l1:', id(l1), [id(l1[0]),id(l1[1])])
print('l2:', id(l2), [id(l2[0]),id(l2[1])])
level 2
l1: 139753183621520 [7394720, 139753182280032]
l2: 139753181521472 [7394720, 139753182280032]

(시점2)

l2[1] += (33,)
l1,l2
([3, (66, 55, 44)], [3, (66, 55, 44, 33)])
print('level 1')
print('l1:', id(l1))
print('l2:', id(l2))
level 1
l1: 139753183621520
l2: 139753181521472
print('level 2')
print('l1:', id(l1), [id(l1[0]),id(l1[1])])
print('l2:', id(l2), [id(l2[0]),id(l2[1])])
level 2
l1: 139753183621520 [7394720, 139753182280032]
l2: 139753181521472 [7394720, 139753174874064]

주소 139753182280032에 있는 값을 바꾸고 싶지만 불변형이라 못바꿈 \(\to\) 그냥 새로 만들자. 그래서 그걸 139753174874064에 저장하자.

Shallow-copy vs Deep-copy

- 암기용문구: “shallow copy는 껍데기만 복사한다. deep copy는 내부까지 복사한다.”

- 일부교재에서는 경우에 따라 shallow copy가 선호될 수 있다는 식으로 설명되어있으나 솔직히 대부분 코드에서 shallow copy의 동작을 의도하고 코드를 사용하진 않는다. 대부분의 경우에서 shallow copy는 불필요한 side effect을 유발하는 쓸모없는 개념이라 생각한다.

- 그럼 shallow copy의 장점은 무엇인가? shallow copy가 deep copy보다 메모리를 더 적게 사용한다.

## 예제1
lst1 = ['양성준',['최규빈','이영미','최혜미']]
lst2 = lst1.copy()
## 예제2 
lst1 = ['양성준',['최규빈','이영미','최혜미']]
lst2 = copy.deepcopy(lst1)
  • 예제1: 4+1+2 = 7개의 공간 사용
  • 예제2: 4+2+2 = 8개의 공간 사용

- 분노: 참조인지 카피인지 구분하는것도 힘든데, 카피도 shallow 인지 deep 인지 구분해서 사용해야해? 겨우 저 메모리때문에?

요약

- 파이썬은 메모리를 아끼기 위해서 shallow copy라는 이상한 행동을 한다.

- 통찰1: 그런데 오묘하게도 [1,2,3,4,5,6]와 같이 중첩된 리스트가 아니라면 문제가 되지 않음. (메모리는 아끼면서 문제가 되지 않는다?? 천재인데??)

  • 중첩된 리스트가 아닐 경우는 shallow copy = deep copy 임.

- 통찰2: 생각해보니까 모든 자료형이 불변형인 경우에도 문제가 되지 않음. (R은 모든 자료형이 불변형이다)

- 문제상황요약: [[1,2],[3,4]] 와 같이 리스트에 리스트가 포함된 형태라면 문제가 생긴다. (이건 개선이 필요함)

  • 개선1: 깊은복사
  • 개선2: 넘파이

numpy

import numpy as np

2차원의 실체

- 2차원 array a,b를 선언하자.

a = np.array([[11,22,33,44]]).reshape(2,2)
b = np.array([[11,22,33,44,55,66]]).reshape(2,3)
c = np.array([11,22,33,44]).reshape(4,1)
d = np.array([11,22,33,44])

- a,b,c,d 속성비교

a.shape, b.shape, c.shape, d.shape ## 차원 
((2, 2), (2, 3), (4, 1), (4,))
a.strides, b.strides, c.strides, d.strides ## 차원이랑 관련이 있어보임.. + 8의 배수 
((16, 8), (24, 8), (8, 8), (8,))

- strides는 무엇?

  • strides: (다음 행으로 가기위해서 JUMP해야하는 메모리 공간수, 다음 열로 가기위해서 JUMP해야하는 메모리 공간수)

- 사실 a,b,c,d 는 모두 1차원으로 저장되어있음. (중첩된 리스트꼴이 아니라)

참조

- a를 선언, b는 a의 참조

a=np.array([[1,2],[3,4]])
b=a ## 참조 
a
array([[1, 2],
       [3, 4]])
b
array([[1, 2],
       [3, 4]])
a.shape
(2, 2)
b.shape
(2, 2)

- a의 shape을 바꾸어보자 \(\to\) b도 같이 바뀐다

a.shape = (4,)
a
array([1, 2, 3, 4])
b
array([1, 2, 3, 4])
id(a),id(b)
(139753605843920, 139753605843920)

view

- a를 선언, b는 a의 view

a=np.array([[1,2],[3,4]]) 
b=a.view() ## shallow copy 라고 부르기도 한다. 
a
array([[1, 2],
       [3, 4]])
b
array([[1, 2],
       [3, 4]])
a.shape
(2, 2)
b.shape
(2, 2)
a.shape= (4,1)
a
array([[1],
       [2],
       [3],
       [4]])
b
array([[1, 2],
       [3, 4]])
id(a), id(b)
(139753151516464, 139753151516560)

- 그런데..

a[0]=100
a
array([[100],
       [  2],
       [  3],
       [  4]])
b
array([[100,   2],
       [  3,   4]])

- 출생의 비밀

b
array([[100,   2],
       [  3,   4]])
b.base
array([[100],
       [  2],
       [  3],
       [  4]])
  • ? 이거 바뀐 a아니야?
id(b.base), id(a)
(139753151516464, 139753151516464)

- View

  • b가 a의 뷰라는 의미는, b가 a를 소스로하여 만들어진 오브젝트란 의미이다.
  • 따라서 이때 b.base는 a가 된다.
  • b는 자체적으로 데이터를 가지고 있지 않으며 a와 공유한다.
  • 이러한 의미에서 view를 shallow copy 라고 부른다. (stride, shape과 같은 껍데기만 새로 생성, 데이터는 a에 저장된 값들을 그대로 사용)

note1 원본 ndarray의 일 경우는 .base가 None으로 나온다.

a.base

note2 b.base의 shpae과 b의 shape은 아무 관련없다.

b.shape
(2, 2)
b.base.shape # a.shape과 같음
(4, 1)

copy

- a를 선언, b는 a의 copy

a=np.array([[1,2],[3,4]])
b=a.copy() # 껍데기를 새로 생성 (strides, shape) + 데이터도 a와 독립적으로 새로 생성하여 따로 메모리에 저장함. 
id(a),id(b)
(139753151672016, 139753151660368)

- a의 shape을 바꿔도 b에는 적용되지 않음

a.shape = (4,1)
a
array([[1],
       [2],
       [3],
       [4]])
b
array([[1, 2],
       [3, 4]])

- 그리고 a[0]의 값을 바꿔도 b에는 적용되지 않음.

a[0]=100
a
array([[100],
       [  2],
       [  3],
       [  4]])
b
array([[1, 2],
       [3, 4]])

- b의 출생을 조사해보니..

a.base,b.base
(None, None)

출생의 비밀은 없었다. 둘다 원본.

- .view() 는 껍데기만 새로생성 // .copy() 는 껍데기와 데이터를 모두 새로 생성

Appendix: .copy의 한계(?)

(관찰)

a=np.array([1,[1,2]],dtype='O')
b=a.copy()
print('시점1')
print('a=',a)
print('b=',b)

a[0]=222
print('시점2')
print('a=',a)
print('b=',b)

a[1][0]=333
print('시점2')
print('a=',a)
print('b=',b)
시점1
a= [1 list([1, 2])]
b= [1 list([1, 2])]
시점2
a= [222 list([1, 2])]
b= [1 list([1, 2])]
시점2
a= [222 list([333, 2])]
b= [1 list([333, 2])]
  • 왜 또 시점2에서는 a와 b가 같이 움직여?

해결책: 더 깊은 복사

a=np.array([1,[1,2]],dtype='O')
b=copy.deepcopy(a)
print('시점1')
print('a=',a)
print('b=',b)

a[0]=222
print('시점2')
print('a=',a)
print('b=',b)

a[1][0]=333
print('시점2')
print('a=',a)
print('b=',b)
시점1
a= [1 list([1, 2])]
b= [1 list([1, 2])]
시점2
a= [222 list([1, 2])]
b= [1 list([1, 2])]
시점2
a= [222 list([333, 2])]
b= [1 list([1, 2])]

- 중간요약

  • 사실 b=a.copy()는 에서 .copy()는 온전한 deep-copy가 아니다.
  • b=a.copy() 에서 .copy()a의 데이터정보를 shallow-copy 한다.
  • 그래서 a의 데이터가 중첩구조를 가지는 경우는 온전한 deep-copy가 수행되지 않는다.
  • 그런데 일반적으로 넘파이를 이용할때 자주 사용하는 데이터 구조인 행렬, 텐서등은 데이터가 중첩구조를 가지지 않는다. (1차원 array로만 저장되어 있음)
  • 따라서 행렬, 텐서에 한정하면 .copy()는 온전한 deep-copy라고 이해해도 무방하다. <– 이것만 기억해!

별명, 뷰, 카피

- test 함수 작성

def test(a,b): 
    if id(a) == id(b): 
        print("별명")
    elif id(a) == id(b.base) or id(a.base)==id(b): 
        print("뷰")
    elif (id(a.base)!=id(None) and id(b.base)!=id(None)) and id(a.base) == id(b.base):
        print("공통의 base를 가짐")
    else: 
        print("카피, 혹은 아무 관련없는 오브젝트") 

- 잘 동작하나?

(테스트1)

a=np.array([1,2,3,4])
b=a
test(a,b)
별명

(테스트2)

a=np.array([1,2,3,4])
b=a.view()
test(a,b)

(테스트3)

a=np.array([1,2,3,4])
b=a.view()
c=a.view()
test(b,c)
공통의 base를 가짐
test(a,b)
test(a,c)

(테스트4)

a=np.array([1,2,3,4])
b=a.copy()
test(a,b)
카피, 혹은 아무 관련없는 오브젝트

결론

- 우리가 사용했던 어떠한 것들이 뷰가 나올지 카피가 나올지 잘 모른다. (그래서 원리를 이해해도 대응할 방법이 사실없음)

예시1

a=np.array([1,2,3,4])
b=a[:3]
a
array([1, 2, 3, 4])
b
array([1, 2, 3])
test(a,b)
c=a[[0,1,2]]
c
array([1, 2, 3])
test(a,c)
카피, 혹은 아무 관련없는 오브젝트

예시2

a=np.array([[1,2],[3,4]])
a
array([[1, 2],
       [3, 4]])
b=a.flatten()
c=a.ravel()
d=a.reshape(-1)
test(a,b)
카피, 혹은 아무 관련없는 오브젝트
test(a,c)
test(a,d)
test(c,d)
공통의 base를 가짐
test(b,c)
카피, 혹은 아무 관련없는 오브젝트