13wk-1: 깊은복사와 얕은복사

Author

최규빈

Published

June 21, 2023

모르고 살았다면 더 좋았을 내용

강의영상

youtube: https://youtube.com/playlist?list=PLQqh36zP38-x1VTi4A3DVjHwcc56J9mwj

import

import numpy as np
import pandas as pd 

Introduction

예제1: 비상식적인 append

포인트: 이상한 일의 관찰

- 원소의 추가: + 이용

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

- 원소의 추가 .append 이용

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

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)
140237129319488
id(b)
140237129319488

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

포인트: (1) 포스트잇 개념의 확실한 이해 (2) 할당문을 새로운 시각으로 해석하는 연습 (3) “생성->할당”과 “참조/에일리어싱”의 구분

a=[1,2,3] # 우변: 생성된 오브젝트, 좌변: 이름 
b=a # 우변: 가져온 오브젝트, 좌변: 별명 --> 참조, 에일리어싱(별칭부여)이라고 한다
a.append(4) # a라는 오브젝트를 직접변경
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)
(140237129249664, 140237129249664, 140237128836544)

예제2

선행지식: “생성->할당” 과 “참조/에일리어싱”의 구분

포인트: 재할당의 이해!!

(관찰)

a=[1,2,3] # 생성->할당
b=a # 참조/에일리어싱 
a=a+[4] # 생성->재할당 
print('a=',a)
print('b=',b)
a= [1, 2, 3, 4]
b= [1, 2, 3]

(해설)

id(a),id(b)
(140237129283584, 140237129350912)
  • 포인트: [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)
(140571068242688, 140571068242688)

이해는 되지만 우리가 원한건 이런게 아니야

예제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) ## 드디어 주소가 달라졌다.
(140571068242688, 140571068242240)

예제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)
(140571077644352, 140571068253376)
  • l1이랑 l2의 주소도 다르게 나오는데 왜 또 참조한것마냥 l1과 l2가 같이 바뀌고 있지?

나는 진정한 복사(=깊은복사)를 하고싶다

Shallow copy의 이해

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

예제1

선행지식: 이전까지 모든것

포인트: 0차원 자료형의 메모리 구조 이해, 1차원 자료형의 메모리 구조를 위한 떡밥

(관찰+해설)

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

메모리 상황

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

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

예제2

선행지식: 이전까지 모든것, 0차원 자료형의 메모리저장상태 이해

포인트: (1) 1차원 자료형의 메모리 구조 이해 (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]
(140527746917824, [7585472, 7585504, 140528016796752])
id(b), [id(b[0]),id(b[1]),id(b[2])] # b=[1,2,2222] 
(140527746917568, [7585472, 7585504, 140528016796144])
a.append(4)
a
[1, 2, 2222, 4]
b
[1, 2, 2222]
id(a)
140527746917824

메모리상황

  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

선행지식: 이전까지 모든 것

포인트: l2=l1l2=l1.copy() 의 차이점

(관찰)

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

선행지식: 이전까지 모든것, .copy()의 동작원리

포인트: .copy()의 동작원리 재학습

(관찰)

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

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

선행지식: 이전까지 모든것, .copy()의 동작원리

포인트: (1) .copy()의 한계, (2) 얕은복사라는 명칭의 유래

(관찰)

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

선행지식: 이전까지 모든것, 얕은복사

포인트: (1) 깊은복사 (2) 복사의 레벨을 이해 (3) 얕은복사 = 1단계 깊은복사

- 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: 140137133270656
l2: 140137132727232
  • 레벨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: 140137133270656 [7585536, 140137133267712]
l2: 140137132727232 [7585536, 140137133267456]
  • 레벨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: 140137133270656 [7585536, [7587552, 140137133704320]]
l2: 140137132727232 [7585536, [7587552, 140137137410624]]
  • 레벨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: 140137133470528
l2: 140137137411136
  • 레벨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: 140137133470528 [7585536, 140137133703424]
l2: 140137137411136 [7585536, 140137133703424]
  • 레벨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: 140137133470528 [7585536, [7587552, 140137137410880]]
l2: 140137137411136 [7585536, [7587552, 140137137410880]]
  • 레벨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: 140137133223232
l2: 140137133223232
  • 레벨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: 140137133223232 [7585536, 140137133698560]
l2: 140137133223232 [7585536, 140137133698560]
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: 140137133223232 [7585536, [7587552, 140137133438144]]
l2: 140137133223232 [7585536, [7587552, 140137133438144]]

Note: 문헌에 따라서 shallow copy를 level1 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

선행지식: 이전까지 모든것

포인트: 재할당의 활용하여 얕은복사의 한계점 극복, 예제4를 위한 떡밥

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

선행지식: 이전까지 모든것

포인트: 재할당으로 인해 메모리주소가 틀어짐을 이용한 트릭예제, 예제5의 떡밥예제

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]]

예제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 메소드처럼)

불변형 객체

예제1: 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: 140006812656640
l2: 140006812645888
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: 140006812656640 [7585536, 140006812590400]
l2: 140006812645888 [7585536, 140006812590400]

(시점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: 140006812656640
l2: 140006812645888
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: 140006812656640 [7585536, 140006812590400]
l2: 140006812645888 [7585536, 140006813422272]

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

Shallow-copy vs Deep-copy

- 암기용문구: “얕은복사는 껍데기만 복사한다. 깊은복사는 내부까지 복사한다.”

- 일부교재에서는 경우에 따라 얕은복사가 선호될 수 있다는 식으로 설명되어있으나 솔직히 대부분 코드에서 얕은복사의 동작을 정확하게 의도하고 코드를 사용하진 않는다. 대부분의 경우에서 얕은복사는 불필요한 side effect을 유발하는 쓸모없는 개념이라 생각한다. (복사를 이미 하려고 마음먹었으면 보통 깊은복사를 생각함, 그게 아니라면 차라리 참조를 썼겠지..)

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

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

요약

- 파이썬은 메모리를 아끼기 위해서 얕은복사라는 이상한 행동을 한다. (하지만 우리는 보통 얕은복사를 원하지 않아. 우리는 깊은복사만 쓰고 싶음!)

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

  • 1차원 리스트의 경우(= 중첩된 리스트가 아닐 경우)는 “레벨1 수준의 깊은복사”만 있으면 충분하다. 따라서 “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]) # 1d

- 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()
B1
array([[1, 2],
       [3, 4]])
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]]))

- 해방: 넘파이를 쓰면 copy.deepcopy()를 쓰지 않아도 된다.

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

  • numpy 한정 .copy() 는 copy모듈의 deep copy와 동등한 효과를 준다. 하지만 실제로는 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라는 말을 찾아볼 수 없음.

- 정리 (넘파이한정)

  • nparray.copy(): 실제로는 shallow copy, 그런데 느낌은 deep copy
  • nparray.view(): 실제로는 shallow copy 보다 더 얕은 단계의 카피, 그런데 느낌은 shallow copy

참조

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

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)
(139679960161232, 139679932937872)

- 그런데..

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

- 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)
(139679932937584, 139679932937584)
  • X.TX의 view 이다.
X.T @ X ## 실제로 X.T를 메모리공간에 새로 만들어 숫자를 저장하지않고 X.T @ X를 계산할 수 있음 (R과차이점) 
array([[124.15127928,  -0.45772606],
       [ -0.45772606,  79.17005817]])

copy

- a를 선언, b는 a의 copy

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

- 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가 같이 움직여?

해결책: 더 깊은 복사

import copy
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\) 그냥 방어적 프로그래밍이 최선인듯