import numpy as np
import pandas as pd
A1: 깊은복사와 얕은복사
모르고 살았어도 좋았을 내용
import
Introduction
비상식적인 append
-
아래의 코드를 관찰하자.
=[1,2,3]
a=a
b=a+[4] a
현재 a,b의 출력결과는?
print('a=', a)
print('b=', b)
a= [1, 2, 3, 4]
b= [1, 2, 3]
-
이제 다시 아래의 코드를 관찰하자.
=[1,2,3]
a=a
b4) a.append(
현재 a,b의 출력결과는?
print('a=', a)
print('b=', b)
a= [1, 2, 3, 4]
b= [1, 2, 3]
append의 동작원리: 틀린상상
-
상자로서의 변수: 변수가 데이터를 저장하는 일종의 상자와 같다. <– 아주 흔한 오해 (Fluent Python)
흔히 비유하는 ‘상자로서의 변수’ 개념이 실제로는 객체지향적 언어에서 참조변수를 이해하는 데 방해가 된다.
-
“상자로서의 변수” 관점에서 아래의 코드를 해석하자. (일단 아래의 해석들이 틀린해석이라는 사실을 명심할 것)
=[1,2,3]
a=a
b4) a.append(
a,b라는 변수들은 메모리에 어떻게 저장이 되어있을까?
상상력을 조금 발휘하면 아래와 같이 여길 수 있다.
메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.
아래를 실행하였을 경우
=[1,2,3] a
- 메모리주소1에 존재하는 방을 a라고 하고, 그 방에 [1,2,3]을 넣는다.
- 아래를 실행하였을 경우
=a b
- 메모리주소2에 존재하는 방을 b라고 하고, 그 방에 a를 넣어야하는데, a는 [1,2,3]이니까 [1,2,3]을 넣는다.
- 아래를 실행하면
4) a.append(
- 방 a로가서 [1,2,3]을 [1,2,3,4]로 바꾼다.
- 그리고 방 b에는 아무것도 하지 않는다.
-
R에서는 맞는 비유인데, 파이썬은 적절하지 않은 비유이다.
틀린이유
id(a)
139753545242336
id(b)
139753545242336
실제로는 a,b가 저장된 메모리 주소가 동일함
append의 동작원리: 올바른 상상
파이썬에서의 변수는 자바에서의 참조변수와 같으므로 변수는 객체에 붙은 레이블이라고 생각하는 것이 좋다.
-
파이썬에서는 아래가 더 적절한 비유이다.
메모리는 변수를 담을 방이 여러개 있는 호텔이라고 생각하자.
아래를 실행하였을 경우
=[1,2,3] a
- 메모리주소
139753545242336
에서 [1,2,3]을 생성 - 방
139753545242336
의 방문에a
라는 포스트잇을 붙인다. - 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서
a
라는 포스트잇이 붙은 방을 찾아가면 된다.
- 아래를 실행하였을 경우
=a b
a
라는 포스트잇이 지칭하는 객체를 가져옴. 그리고 그 객체에b
라는 포스트잇을 붙인다.- 쉽게말하면
b
라는 포스트잇을 방139753545242336
의 방문에 붙인다는 이야기. - 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서
a
라는 포스트잇이 붙어 있거나b
라는 포스트잇이 붙어있는 방을 찾아가면 된다.
- 아래를 실행하면
4) a.append(
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
=[1,2,3]
a=a
b4)
a.append(=[1,2,3,4] c
여기에서 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
(관찰)
=[1,2,3] # 할당
a=a # 에일리어싱
b=[1,2,3]+[4] # 재할당
aprint('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
=1+2021
aid(a)
139753546122608
=2023-1
bid(b)
139753545299280
id(2022)
139753545299472
- 당연한결과임.
예제2: 이제 다 이해했다고 생각했는데..
=1+2
aid(a)
7394720
=4-1
bid(b)
7394720
- id(a)와 id(b)가 왜 똑같지..?
(해설) 파이썬의 경우 효율성을 위해서 -5~256까지의 정수를 미리 저장해둠.
id(3)
7394720
- 3은 언제나 7394720에 지박령마냥 밖혀있음
.copy()
의 사용 (shallow copy의 사용)
예제1
(관찰) 아래의 예제를 살펴보자. 참조를 제대로 이해했다면 아래의 예제는 자연스럽게 이해가능할 것임.
= [3, [66,55,44]]
l1 = l1
l2 print('시점1')
print('l1=',l1)
print('l2=',l2)
0]=4
l1[print('시점2')
print('l1=',l1)
print('l2=',l2)
5)
l2.append(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]
(해설)
= [3, [66,55,44]]
l1 = l1 l2
id(l1),id(l2)
(139753545268832, 139753545268832)
이해는 되지만 우리가 원한건 이런게 아니야
예제2: R과 같이 = 를 쓰고 싶다면?
(관찰)
= [3, [66,55,44]]
l1 = l1.copy()
l2 print('시점1')
print('l1=',l1)
print('l2=',l2)
0]=4
l1[print('시점2')
print('l1=',l1)
print('l2=',l2)
5)
l2.append(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]
(해설)
= [3, [66,55,44]]
l1 = l1.copy() l2
id(l1),id(l2) ## 드디어 주소가 달라졌다.
(140346713602720, 140346713599104)
예제3: 이제 다 이해했다고 생각했는데..
(관찰)
= [3,[66,55,44]]
l1 = l1.copy()
l2 1].append(33)
l1[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
(관찰+해설)
=2222
a=2222 b
id(a),id(b)
(139753545300880, 139753545301808)
메모리 상황
- 2222라는 오브젝트가 어떤공간(
139753545300880
)에 생성되고 그 공간에a
라는 라벨이 붙음 - 2222라는 오브젝트가 어떤공간(
139753545301808
)에 생성되고 그 공간에b
라는 라벨이 붙음
즉 -5~256 이외의 2개의 메모리 공간을 추가적으로 사용
예제2
(관찰)
=[1,2,2222]
a=[1,2,2222]
b4)
a.append(print('a=',a)
print('b=',b)
a= [1, 2, 2222, 4]
b= [1, 2, 2222]
(해설)
=[1,2,2222]
a=[1,2,2222] b
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])
4) a.append(
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
(관찰)
= [3,[66,55,44]]
l1 = l1.copy()
l2 0] = 7777
l1[print('l1=',l1)
print('l2=',l2)
l1= [7777, [66, 55, 44]]
l2= [3, [66, 55, 44]]
(해설)
= [3,[66,55,44]]
l1 = l1.copy() l2
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
라는 포스트잇 생성.
0] = 7777
l1[ 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
(관찰)
= [3,[66,55,44]]
l1 = l1.copy()
l2 7777)
l1.append(print('l1=',l1)
print('l2=',l2)
l1= [3, [66, 55, 44], 7777]
l2= [3, [66, 55, 44]]
(해설)
= [3,[66,55,44]]
l1 = l1.copy()
l2 7777) l1.append(
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: 우리를 힘들게 했던 그 예제.
(관찰)
= [3,[66,55,44]]
l1 = l1.copy()
l2 1].append(7777)
l1[print('l1=',l1)
print('l2=',l2)
l1= [3, [66, 55, 44, 7777]]
l2= [3, [66, 55, 44, 7777]]
(해설-시점1)
= [3,[66,55,44]]
l1 = l1.copy() l2
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)
1].append(7777) l1[
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교육을 실시하고자함. 특강팀은 우선 신임교수들로 구성.
= 신임교수.copy()
R특강팀 R특강팀
['최규빈', '이영미', '박혜원']
-
R특강팀에 최혜미
교수님 추가. (그렇지만 최혜미교수님이 막내는 아니야.. // 참조와 shallow copy의 차이점)
"최혜미") R특강팀.append(
R특강팀, 신임교수, 막내들
(['최규빈', '이영미', '박혜원', '최혜미'], ['최규빈', '이영미', '박혜원'], ['최규빈', '이영미', '박혜원'])
-
R특강팀에서 양성준 교수를 추가하여 파이썬 특강팀을 구성 (R특강팀의 구분을 위해서 중첩리스트 구조로 만들자)
= [R특강팀, "양성준"]
파이썬특강팀 파이썬특강팀
[['최규빈', '이영미', '박혜원', '최혜미'], '양성준']
-
이영미교수는 다른 일이 많아서 R특강 팀에서 제외됨. (그럼 자연히 파이썬에서도 제외됨!!)
"이영미") R특강팀.remove(
R특강팀, 파이썬특강팀
(['최규빈', '박혜원', '최혜미'], [['최규빈', '박혜원', '최혜미'], '양성준'])
하지만 이영미교수는 여전히 신임교수이면서 막내들임
신임교수, 막내들
(['최규빈', '이영미', '박혜원'], ['최규빈', '이영미', '박혜원'])
-
새로운 교수로 “손흥민”이 임용됨.
"손흥민") 막내들.append(
막내들, 신임교수
(['최규빈', '이영미', '박혜원', '손흥민'], ['최규빈', '이영미', '박혜원', '손흥민'])
-
그렇다고 해서 손흥민 교수가 바로 R이나 파이썬 특강팀에 자동소속되는건 아님
R특강팀, 파이썬특강팀
(['최규빈', '박혜원', '최혜미'], [['최규빈', '박혜원', '최혜미'], '양성준'])
Deep copy
예제1: Motivation example
-
아래의 상황을 다시 생각해보자.
= ["양성준",["최규빈","이영미","최혜미"]]
파이썬특강팀 = 파이썬특강팀.copy()
ADSP특강팀 -1].remove("이영미") 파이썬특강팀[
파이썬특강팀, ADSP특강팀
(['양성준', ['최규빈', '최혜미']], ['양성준', ['최규빈', '최혜미']])
이슈: 이영미교수가 파이썬특강에서 제외되면서 ADSP특강팀에서도 제외되었음. 그런데 사실 이영미교수가 파이썬특강팀에서만 제외되길 원한 것이지 ADSP특강팀에서 제외되길 원한게 아닐수도 있음.
해결: Deep copy의 사용
import copy
= ["양성준",["최규빈","이영미","최혜미"]]
파이썬특강팀 = copy.deepcopy(파이썬특강팀)
ADSP특강팀 -1].remove("이영미") 파이썬특강팀[
파이썬특강팀, ADSP특강팀
(['양성준', ['최규빈', '최혜미']], ['양성준', ['최규빈', '이영미', '최혜미']])
예제2
-
deepcopy
= [3,[66,[55,44]]]
l1 = copy.deepcopy(l1) l2
1][1].append(33) l2[
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
= [3,[66,[55,44]]]
l1 = l1.copy() l2
1][1].append(33) l2[
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]
의 메모리 주소도 같음!!
-
비교를 위한 참조
= [3,[66,[55,44]]]
l1 = l1 l2
1][1].append(33) l2[
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
-
아래의 코드결과를 예측하라. 결과가 나오는 이유를 설명하라.
= [3,[66,55,44]]
l1= l1.copy()
l2-1].append(33) l1[
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
-
아래의 코드결과를 예측하라. 결과가 나오는 이유를 설명하라.
= [3,[66,55,44]]
l1= l1.copy()
l2-1] = l1[-1]+[33] l1[
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44]]
- 포인트:
l1[-1]+[33]
가 실행되는 순간 새로운 오브젝트가 생성되고 이 새로운 오브젝트가 l1의 마지막 원소에 새롭게 할당된다.
예제3
= [3,[66,55,44]]
l1= l1.copy()
l2-1] = l1[-1]+[33]
l1[-1].remove(33) l1[
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
= [3,[66,55,44]]
l1= l1.copy()
l2-1] = l1[-1]+[33]
l1[-1].remove(33)
l1[-1].append(33) l1[
(잘못된 상상) 아래의 코드와 결과가 같을거야!!
= [3,[66,55,44]]
l1= l1.copy()
l2# l1[-1] = l1[-1]+[33]
# l1[-1].remove(33)
-1].append(33) l1[
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]
(하지만 현실은)
= [3,[66,55,44]]
l1= l1.copy()
l2-1] = l1[-1]+[33]
l1[-1].remove(33)
l1[-1].append(33) l1[
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44]]
- 포인트: 예제3을 이해했다면 그냥 이해되는것
예제5
= [3,[66,55,44]]
l1= l1.copy()
l2-1] += [33] # l1[-1] = l1[-1]+[33]
l1[-1].remove(33)
l1[-1].append(33) l1[
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
-
우리는 이제 아래의 내용은 마스터함
= [3,[66,55,44]]
l1= l1.copy()
l2-1] += [33] # l1[-1].append(33)이랑 같은거.. l1[
print('l1=', l1)
print('l2=', l2)
l1= [3, [66, 55, 44, 33]]
l2= [3, [66, 55, 44, 33]]
-
아래의 결과를 한번 예측해볼까?
=[3,(66,55,44)]
l1=l1.copy()
l21] += (33,) l2[
print('l1=', l1)
print('l2=', l2)
l1= [3, (66, 55, 44)]
l2= [3, (66, 55, 44, 33)]
해설
(시점1)
=[3,(66,55,44)]
l1=l1.copy() l2
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)
1] += (33,) l2[
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 = lst1.copy()
lst2 ## 예제2
= ['양성준',['최규빈','이영미','최혜미']]
lst1 = copy.deepcopy(lst1) lst2
- 예제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를 선언하자.
= np.array([[11,22,33,44]]).reshape(2,2)
a = np.array([[11,22,33,44,55,66]]).reshape(2,3)
b = np.array([11,22,33,44]).reshape(4,1)
c = np.array([11,22,33,44]) d
-
a,b,c,d 속성비교
## 차원 a.shape, b.shape, c.shape, d.shape
((2, 2), (2, 3), (4, 1), (4,))
## 차원이랑 관련이 있어보임.. + 8의 배수 a.strides, b.strides, c.strides, d.strides
((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?!
=[[1,2],[3,4]]
A1=A1.copy()
A2=np.array([[1,2],[3,4]])
B1=B1.copy() B2
0][0]=11
A2[0][0]=11 B2[
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의 참조
=np.array([[1,2],[3,4]])
a=a ## 참조 b
a
array([[1, 2],
[3, 4]])
b
array([[1, 2],
[3, 4]])
a.shape
(2, 2)
b.shape
(2, 2)
-
a의 shape을 바꾸어보자 \(\to\) b도 같이 바뀐다
= (4,) a.shape
a
array([1, 2, 3, 4])
b
array([1, 2, 3, 4])
id(a),id(b)
(139753605843920, 139753605843920)
view
-
a를 선언, b는 a의 view
=np.array([[1,2],[3,4]])
a=a.view() ## 어떤 블로그등에서는 shallow copy 라고 부르기도 한다. b
a
array([[1, 2],
[3, 4]])
b
array([[1, 2],
[3, 4]])
a.shape
(2, 2)
b.shape
(2, 2)
= (4,1) a.shape
a
array([[1],
[2],
[3],
[4]])
b
array([[1, 2],
[3, 4]])
id(a), id(b)
(140105048258768, 140105048259152)
-
그런데..
0]=100 a[
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)
# a.shape과 같음 b.base.shape
(4, 1)
-
numpy에서 view를 사용하는 예시 (transpose)
= np.random.normal(size=[100,2])
X id((X.T).base), id(X)
(140104289713104, 140104289713104)
X.T
는X
의 view 이다.
@ X ## 실제로 X.T를 메모리공간에 새로 만들어 숫자를 저장하지않고 X.T @ X를 계산할 수 있음 (R과차이점) X.T
array([[81.26122629, 4.44010058],
[ 4.44010058, 82.45450712]])
copy
-
a를 선언, b는 a의 copy
=np.array([[1,2],[3,4]])
a=a.copy() # 껍데기를 새로 생성 (strides, shape) + 데이터도 a와 독립적으로 새로 생성하여 따로 메모리에 저장함. b
id(a),id(b)
(139753151672016, 139753151660368)
-
a의 shape을 바꿔도 b에는 적용되지 않음
= (4,1)
a.shape a
array([[1],
[2],
[3],
[4]])
b
array([[1, 2],
[3, 4]])
-
그리고 a[0]의 값을 바꿔도 b에는 적용되지 않음.
0]=100 a[
a
array([[100],
[ 2],
[ 3],
[ 4]])
b
array([[1, 2],
[3, 4]])
-
b의 출생을 조사해보니..
a.base,b.base
(None, None)
출생의 비밀은 없었다. 둘다 원본.
-
.view()
는 껍데기만 새로생성 // .copy()
는 껍데기와 데이터를 모두 새로 생성
Appendix: .copy의 한계(?)
(관찰)
=np.array([1,[1,2]],dtype='O')
a=a.copy()
bprint('시점1')
print('a=',a)
print('b=',b)
0]=222
a[print('시점2')
print('a=',a)
print('b=',b)
1][0]=333
a[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가 같이 움직여?
해결책: 더 깊은 복사
=np.array([1,[1,2]],dtype='O')
a=copy.deepcopy(a)
bprint('시점1')
print('a=',a)
print('b=',b)
0]=222
a[print('시점2')
print('a=',a)
print('b=',b)
1][0]=333
a[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라고 이해해도 무방하다. <– 이것만 기억해!
요약
아래를 구분할 수 있으면 잘 이해한 것!!
= np.array(...) # arr -- [arr.shape, arr.strides, arr.base, ... ]
arr = arr
arr2 = arr.view()
arr2 = arr.copy()
arr2 = copy.deepcopy(arr) arr2
별명, 뷰, 카피
-
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)
=np.array([1,2,3,4])
a=a b
test(a,b)
별명
(테스트2)
=np.array([1,2,3,4])
a=a.view() b
test(a,b)
뷰
(테스트3)
=np.array([1,2,3,4])
a=a.view()
b=a.view() c
test(b,c)
공통의 base를 가짐
test(a,b)
뷰
test(a,c)
뷰
(테스트4)
=np.array([1,2,3,4])
a=a.copy() b
test(a,b)
카피, 혹은 아무 관련없는 오브젝트
결론
-
참조, 뷰, 카피의 개념을 잘 알고 있고 때에 따라 이들을 적절하게 사용하며 효율적으로 메모리를 쓰고 싶을것 같음. 하지만 이건 불가능한 소망임.
-
우리가 사용했던 어떠한 것들이 뷰가 나올지 카피가 나올지 잘 모른다. (그래서 원리를 이해해도 대응할 방법이 사실없음)
예시1
=np.array([1,2,3,4])
a=a[:3] b
a
array([1, 2, 3, 4])
b
array([1, 2, 3])
test(a,b)
뷰
=a[[0,1,2]]
c c
array([1, 2, 3])
test(a,c)
카피, 혹은 아무 관련없는 오브젝트
예시2
=np.array([[1,2],[3,4]])
a a
array([[1, 2],
[3, 4]])
=a.flatten()
b=a.ravel()
c=a.reshape(-1) d
test(a,b)
카피, 혹은 아무 관련없는 오브젝트
test(a,c)
뷰
test(a,d)
뷰
test(c,d)
공통의 base를 가짐
test(b,c)
카피, 혹은 아무 관련없는 오브젝트
-
심지어 copy인줄 알았던것이 사실 view라서 원치않는 side effect이 생길수 있음. \(\to\) 그냥 방어적 프로그래밍이 최선인듯