A2: 깊은복사와 얕은복사

Author

최규빈

Published

December 14, 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]

append의 동작원리: 틀린상상

- 상자로서의 변수: 변수가 데이터를 저장하는 일종의 상자와 같다. <– 아주 흔한 오해 (Fluent Python)

흔히 비유하는 ‘상자로서의 변수’ 개념이 실제로는 객체지향적 언어에서 참조변수를 이해하는 데 방해가 된다.

- “상자로서의 변수” 관점에서 아래의 코드를 해석하자. (일단 아래의 해석들이 틀린해석이라는 사실을 명심할 것)

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라는 포스트잇이 지칭하는 객체를 가져옴. 그리고 그 객체에 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: 같은 value, 다른 id

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특강 팀에서 제외됨. (그럼 자연히 파이썬에서도 제외됨!!)

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 (=level 1 deep 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 copy라는 이상한 행동을 한다. (하지만 우리는 shallow copy 를 원하지 않아. 우리는 deep copy 만 쓰고 싶음!)

- 통찰1: 그런데 오묘하게도 [1,2,3,4,5,6]와 같이 1차원 리스트인 경우는 문제가 되지 않음.

  • 1차원 리스트의 경우(= 중첩된 리스트가 아닐 경우)는 level 1 수준에서의 deep copy만 있으면 충분하므로 이때는 shallow copy = deep copy 임.

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

- 문제상황요약: [[1,2],[3,4]] 와 같이 리스트에 리스트가 포함된 형태라면 문제가 생긴다. \(\to\) 그런데 우리가 자주 쓰는 매트릭스가 사실 이러한 중첩된 리스트 구조 아니야?

  • 해결책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,))

- ((16, 8), (24, 8), (8, 8), (8,)) 와 같은 저 숫자들이 도데체 무엇을 의미하는거야?!

  • 사전지식: 컴퓨터는 하나의 숫자를 저장하는데 메모리를 8칸 쓴다.
  • 가정: 만약에 컴퓨터가 1차원으로만 숫자를 저장한다면??
  • strides의 의미: (다음 행으로 가기위해서 JUMP해야하는 메모리 공간수, 다음 열로 가기위해서 JUMP해야하는 메모리 공간수)

- 통찰: strides의 존재로 인해서 유추할 수 있는 것은 a,b,c,d 는 모두 1차원으로 저장되어있다는 사실이다. (중첩된 리스트꼴이 아니라)

- 그렇다면.. shallow copy = deep copy?!

A1=[[1,2],[3,4]]
A2=A1.copy()
B1=np.array([[1,2],[3,4]])
B2=B1.copy()
A2[0][0]=11
B2[0][0]=11
A1,A2
([[11, 2], [3, 4]], [[11, 2], [3, 4]])
B1,B2
(array([[1, 2],
        [3, 4]]),
 array([[11,  2],
        [ 3,  4]]))

- 잠깐 생각좀..

  • A2를 바꿨는데 A1이 같이 바뀌는 것은 의도하지 않은 side effect임.
  • 이러한 side effect가 생기는 이유는 파이썬이 메모리를 저장하기 위해서 shallow copy라는 희한한 짓을 하기 때문임.
  • 이런 side effect을 방지하기 위해서는 deep copy를 써야함. 이 deep copy는 메모리를 더 많이 잡아먹는 단점이 있다.
  • 요약하면 side effect 방지와 메모리사용은 trade off 관계에 있음.
  • 그런데 생각해보니까 B2역시 B1의 shallow copy 임. 따라서 deep copy보다 메모리를 적게씀. 그런데 side effect도 발생하지 않음!?

- B1에서 B2를 만드는 과정은 메모리를 적게 쓰지만 side effect과 가은 문제가 없음! (천재인데..?)

- 용어정리: (필요할까..?)

  • numpy 한정 .copy() 는 copy모듈의 deepcopy와 동등한 효과를 준다. 하지만 실제로는 shallow copy 이다. 공식문서에는 “Note that np.copy is a shallow copy and will not copy object elements within arrays.” 라고 명시되어 있음.
  • 일부 블로그에서 deep copy라고 주장하기도 함. 블로그1, 블로그2, 블로그3 // 블로그2의 경우 참조와 shallow copy도 구분못함..
  • 이따가 view라는 개념도 나올텐데 .copy()를 deep copy라고 주장하는 블로거들 대부분 .view()를 shallow copy 혹은 참조라고 주장한다. 하지만 copy와 view를 설명하는 공식문서에서는 view가 shallow copy라는 말을 찾아볼 수 없음.
  • 사실 좀 애매한게 copy가 shallow copy 와 deep copy 둘만 있는건 아님. 사실 .view().copy() 만 놓고 비교할 때 .view().copy()보다 더 얕은 수준의 복사를 하는것도 사실임 (반대로 .copy().view()보다 더 깊은 수준의 복사를 하는 것도 사실임)

참조

- 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)
(140105048258768, 140105048259152)

- 그런데..

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

- View

  • b가 a의 뷰라는 의미는, b가 a를 소스로하여 만들어진 오브젝트란 의미이다.
  • 따라서 이때 b.base는 a가 된다.
  • b는 자체적으로 데이터를 가지고 있지 않으며 a와 공유한다.

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

a.base

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

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

- numpy에서 view를 사용하는 예시 (transpose)

X = np.random.normal(size=[100,2])
id((X.T).base), id(X)
(140104289713104, 140104289713104)
  • X.TX의 view 이다.
X.T @ X ## 실제로 X.T를 메모리공간에 새로 만들어 숫자를 저장하지않고 X.T @ X를 계산할 수 있음 (R과차이점) 
array([[81.26122629,  4.44010058],
       [ 4.44010058, 82.45450712]])

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

요약

아래를 구분할 수 있으면 잘 이해한 것!!

arr = np.array(...) # arr -- [arr.shape, arr.strides, arr.base, ... ] 
arr2 = arr 
arr2 = arr.view()
arr2 = arr.copy()
arr2 = copy.deepcopy(arr)

별명, 뷰, 카피

- 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)
카피, 혹은 아무 관련없는 오브젝트

- 심지어 copy인줄 알았던것이 사실 view라서 원치않는 side effect이 생길수 있음. \(\to\) 그냥 방어적 프로그래밍이 최선인듯