import numpy as np
import pandas as pd A1: 깊은복사와 얕은복사
모르고 살았어도 좋았을 내용
import
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라는 변수들은 메모리에 어떻게 저장이 되어있을까?
상상력을 조금 발휘하면 아래와 같이 여길 수 있다.
메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.
아래를 실행하였을 경우
a=[1,2,3]- 메모리주소1에 존재하는 방을 a라고 하고, 그 방에 [1,2,3]을 넣는다.
- 아래를 실행하였을 경우
b=a- 메모리주소2에 존재하는 방을 b라고 하고, 그 방에 a를 넣어야하는데, a는 [1,2,3]이니까 [1,2,3]을 넣는다.
- 아래를 실행하면
a.append(4)- 방 a로가서 [1,2,3]을 [1,2,3,4]로 바꾼다.
- 그리고 방 b에는 아무것도 하지 않는다.
- R에서는 맞는 비유인데, 파이썬은 적절하지 않은 비유이다.
틀린이유
id(a)139753545242336
id(b)139753545242336
실제로는 a,b가 저장된 메모리 주소가 동일함
append의 동작원리: 올바른 상상
파이썬에서의 변수는 자바에서의 참조변수와 같으므로 변수는 객체에 붙은 레이블이라고 생각하는 것이 좋다.
- 파이썬에서는 아래가 더 적절한 비유이다.
메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.
아래를 실행하였을 경우
a=[1,2,3]- 메모리주소
139753545242336에서 [1,2,3]을 생성 - 방
139753545242336의 방문에a라는 포스트잇을 붙인다. - 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서
a라는 포스트잇이 붙은 방을 찾아가면 된다.
- 아래를 실행하였을 경우
b=aa라는 포스트잇이 지칭하는 객체를 가져옴. 그리고 그 객체에b라는 포스트잇을 붙인다.- 쉽게말하면
b라는 포스트잇을 방139753545242336의 방문에 붙인다는 이야기. - 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서
a라는 포스트잇이 붙어 있거나b라는 포스트잇이 붙어있는 방을 찾아가면 된다.
- 아래를 실행하면
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라고도 부르고 싶다는 의미인데 이는 마치 별명과 같다. (b는 a의 별명, 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=2222id(a),id(b)(139753545300880, 139753545301808)
메모리 상황
- 2222라는 오브젝트가 어떤공간(
139753545300880)에 생성되고 그 공간에a라는 라벨이 붙음 - 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]
메모리상황
- -5~256까지의 숫자는 미리 메모리에 저장되어 있다. 이중에서 1은
7394656, 2는7394688에 저장되어있음. - 2222가 공간
139753178093776에서 만들어진다. - 어떠한 리스트오브젝트가 공간
139753182327904에서 만들어지고 원소로 [1,2,2222]를 가진다. 이 공간에a라는 포스트잇을 붙인다. - 2222가 공간
139753178095568에서 만들어진다. - 어떠한 리스트오브젝트가 공간
139753173818656에서 만들어지고 원소로 [1,2,2222]를 가진다. 이 공간에b라는 포스트잇을 붙인다. 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])
메모리상황
- -5~256까지의 숫자가 메모리에 저장되어 있다.
- 저장된 숫자중 66,55,44를 묶어서 리스트로 구성하고 이 리스트를 공간
139753183707216에 저장. - 숫자 3과 공간
139753183707216에 저장된 리스트 [66,55,44]를 하나로 묶어서 새로운 리스트를 구성하고 이를 공간139753183437040에 저장. 공간139753183437040에l1이라는 포스트잇 생성. - 공간
139753182311120에l1의 원소들을 모아서 새로운 리스트를 구성함. 공간139753182311120에l2라는 포스트잇 생성.
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])
- l1[0]은 원래 공간
7394720와 binding 되어 있었음.
- 그런데 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 = l1l2[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 np2차원의 실체
- 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]=11A1,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 ## 참조 aarray([[1, 2],
[3, 4]])
barray([[1, 2],
[3, 4]])
a.shape(2, 2)
b.shape(2, 2)
- a의 shape을 바꾸어보자 \(\to\) b도 같이 바뀐다
a.shape = (4,)aarray([1, 2, 3, 4])
barray([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 라고 부르기도 한다. aarray([[1, 2],
[3, 4]])
barray([[1, 2],
[3, 4]])
a.shape(2, 2)
b.shape(2, 2)
a.shape= (4,1)aarray([[1],
[2],
[3],
[4]])
barray([[1, 2],
[3, 4]])
id(a), id(b)(140105048258768, 140105048259152)
- 그런데..
a[0]=100aarray([[100],
[ 2],
[ 3],
[ 4]])
barray([[100, 2],
[ 3, 4]])
- 출생의 비밀
barray([[100, 2],
[ 3, 4]])
b.basearray([[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.basenote2 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.T는X의 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)
aarray([[1],
[2],
[3],
[4]])
barray([[1, 2],
[3, 4]])
- 그리고 a[0]의 값을 바꿔도 b에는 적용되지 않음.
a[0]=100aarray([[100],
[ 2],
[ 3],
[ 4]])
barray([[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=atest(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]aarray([1, 2, 3, 4])
barray([1, 2, 3])
test(a,b)뷰
c=a[[0,1,2]]
carray([1, 2, 3])
test(a,c)카피, 혹은 아무 관련없는 오브젝트
예시2
a=np.array([[1,2],[3,4]])
aarray([[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\) 그냥 방어적 프로그래밍이 최선인듯