- 오늘오전: 자료형, 오브젝트 ..

- 오늘오후: 참조, 얕은복사, 깊은복사

- 내일오전: 뷰와 카피

- 내일오후: 개발환경 비교

참조와 에일리어싱

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

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

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

a,b
([1, 2, 3, 4], [1, 2, 3])

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

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

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

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

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

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

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

(2) 아래를 실행하였을 경우

a=[1,2,3]
  • 메모리주소1에 존재하는 방을 a라고 하고, 그 방에 [1,2,3]을 넣는다.

(3) 아래를 실행하였을 경우

b=a
  • 메모리주소38에 존재하는 방을 b라고 하고, 그 방에 a를 넣어야하는데, a는 [1,2,3]이니까 [1,2,3]을 넣는다.

(4) 아래를 실행하면

a.append(4)
  • 방 a로가서 [1,2,3]을 [1,2,3,4]로 바꾼다.
  • 그리고 방 b에는 아무것도 하지 않는다.

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

id(a)
139851743661952
id(b)
139851743661952

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

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

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

(2) 아래를 실행하였을 경우

a=[1,2,3]
  • 메모리주소139851743661952에서 [1,2,3]을 생성해요
  • 방 139851743661952의 방문에 a라는 포스트잇을 붙인다.
  • 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서 a라는 포스트잇이 붙은 방을 찾아가면 된다.

(3) 아래를 실행하였을 경우

b=a
  • a라는 포스트잇이 있는데, a라는 포스트잇이랑 b라는 포스트잇과 같은 효과를 주도록 한다.
  • 쉽게말하면 b라는 포스트잇을 방 139851743661952의 방문에 붙인다는 이야기.
  • 앞으로 [1,2,3]에 접근하기 위해서는 여러 메모리방중에서 a라는 포스트잇이 붙어 있거나 b라는 포스트잇이 붙어있는 방을 찾아가면 된다.

(4) 아래를 실행하면

a.append(4)
  • a라는 포스트잇이 붙어있는 방으로 가서, 그 내용물 append함수를 써서 4를 추가하라. 즉 내용물 [1,2,3]을 [1,2,3,4]로 바꾸라.
  • 같은방에 a,b라는 포스트잇이 모두 붙어있음. 따라서 b라는 포스트잇이 붙은 방을 찾아가서 내용물을 열어보면 [1,2,3,4]가 나온다.

할당문(=)의 이해

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

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

- b=a

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

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

- 참조: 하나의 메모리 주소에 변수의 이름을 바인딩하는 것을 참조라고 한다.

- 에일리어싱: 하나의 메모리 주소에 여러개의 변수이름을 바인딩 하는 것을 에일리어싱이라고 한다.

id, value

- 아래의 예제를 살펴보자.

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)

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

a=[1,2,3] 
b=a 
a=[1,2,3]+[4] 
a,b
([1, 2, 3, 4], [1, 2, 3])
id(a),id(b)
(139851742145536, 139851743152000)

이터닝

- 참조의 개념은 확실하게 알겠음. 이제 아래의 예제를 살펴보자.

a=1+2 
id(a)
139852067748208
b=4-1
id(b)
139852067748208

이게 왜 똑같지..?

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

id(22)
139852067748816

- 이제 아래의 예제를 살펴보자.

a=1+2021
id(a)
139851741177168
b=2023-1
id(b)
139851741254000
id(2022)
139851741253744

copy (shallow copy)

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

l1 = [3, [66,55,44]]
l2 = l1 
id(l1),id(l2)
(139851740541056, 139851740541056)
l1[0]=4 
l1
[4, [66, 55, 44]]
l2
[4, [66, 55, 44]]
l2.append(5)
l2
[4, [66, 55, 44], 5]
l1
[4, [66, 55, 44], 5]

- R과 같은 방식으로 =를 사용하고 싶다면?

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

- 이제 다 이해했다고 생각했는데..

l1 = [3,[66,55,44]]
l2 = l1.copy()
id(l1),id(l2)
(139851987671936, 139851740745472)
l1[1].append(33)
l1
[3, [66, 55, 44, 33]]
l2
[3, [66, 55, 44, 33]]

왜 또 참조한것마냥 l1과 l2가 같이 바뀌고 있지?

- 위의 문제를 해설하여 보자. (주의: 해설이 좀 길어요)

(상황1)

a=2222
b=2222
id(a),id(b)
(139851743195216, 139851743195440)

현재 메모리 상황

  • -5~256 까지의 숫자는 어딘가에 저장되어 있음
  • 2222라는 오브젝트가 어떤공간에 생성되고 그 공간에 a라는 라벨이 붙음
  • 2222라는 오브젝트가 어떤공간에 생성되고 그 공간에 b라는 라벨이 붙음

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

(상황2) 이제 아래의 상황을 살펴보자

a=[1,2,2222]
b=[1,2,2222]
id(a),id(b)
(139851740010880, 139851742120448)
id(a[0]),id(b[0])
(139852067748144, 139852067748144)
id(a[1]),id(b[1])
(139852067748176, 139852067748176)
id(a[2]),id(b[2])
(139851743195248, 139851743195568)
a.append(4)
a
[1, 2, 2222, 4]
b
[1, 2, 2222]

해설을 다시 해보면 아래와 같다.

  • -5~256까지의 숫자가 메모리에 저장되어 있다.
  • 2222가 어떤 공간에 저장된다. 공간 X1이라고 하자.
  • 리스트오브젝트가 만들어지고 원소로 1,2,2222를 가짐. 이 공간을 a라고 부름.
  • 2222가 어떤 공간에 저장된다. 공간을 X2라고 하자.
  • 리스트오브젝트가 만들어지고 원소로 1,2,2222를 가짐. 이 공간을 b라고 부름.
  • 공간 a에 원소 4를 추가시킴.

즉 -5~256이외에 4개의 메모리 공간을 추가사용

  • X1: 2222
  • X2: 2222
  • a = 1,2,공간X1의값 --> append를 쓰면 1,2,공간X1의값,4
  • b = 1,2,공간X2의값

(상황3)

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

해설을 해보자.

  • -5~256까지의 숫자가 메모리에 저장되어 있다.
  • [66,55,44]가 저장되는 공간이 생성됨. 이 공간을 X1
  • [3,[66,55,44]]가 저장되는 공간이 생성됨. 이 공간을 l1
  • 공간 l2가 만들어지고 그 안에 리스트가 있음. 리스트의 첫번째원소는 3, 두번째원소는 공간X1의 값.
  • 공간X1의 값에 접근하여 33을 추가함. 즉 공간 X1의 값을 [66,55,44]에서 [66,55,44,33]으로 변경

따라서

  • l1 = [3, 공간X1의 값]
  • l2 = [3, 공간X2의 값]
  • l1[1].append(33) 을 하는 순간 공간 x1의 값이 [66,55,44]에서 [66,55,44,33]으로 변경

- 결국 아래의 2개의 코드는 다른 코드임

a=[2222,3333]
b=[2222,3333]
id(a),id(b)
(139851750028864, 139851740431104)
id(a[0]),id(b[0])
(139851743196432, 139851743196336)
id(a[1]),id(b[1])
(139851743196240, 139851743196752)
a=[2222,3333]
b=a.copy()
id(a),id(b)
(139851739986112, 139851739526784)
id(a[0]),id(b[0])
(139851743196912, 139851743196912)
id(a[1]),id(b[1])
(139851743196848, 139851743196848)

- 결국 *.copy() 메소드는 깊은곳까지 복사를 하지 않음. (메모리를 너무 아낀다.)

- 사실 그런데 이렇게 해도 평소에는 티가 잘 안난다.

a=[2222,3333]
b=a.copy()
a
[2222, 3333]
b
[2222, 3333]
b[1]=4444
b
[2222, 4444]
id(a),id(b)
(139851740984000, 139851740712640)
id(a[0]),id(b[0])
(139851743197456, 139851743197456)
id(a[1]),id(b[1])
(139851743197392, 139851743197488)

shallow copy 연습문제

- 예제1: 아래코드의 결과를 (l1,l2의 값, 메모리상황) 예측하라. 결과가 나오는 이유를 설명하라.

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

- 예제2: 아래코드의 결과를 (l1,l2의 값, 메모리상황) 예측하라. 결과가 나오는 이유를 설명하라.

l1= [3,[66,55,44]] ## X1, l1  
l2= l1.copy() ## l2  
l1[1] = l1[1]+[33] ## X2 
l1,l2
([3, [66, 55, 44, 33]], [3, [66, 55, 44]])

- 예제3: 예제2에서 이어서 아래를 실행하라. 아래코드의 결과를 (l1,l2의 값, 메모리상황) 예측하라. 결과가 나오는 이유를 설명하라.

l1[1].remove(33)
l1,l2
([3, [66, 55, 44]], [3, [66, 55, 44]])

- 예제4: 현재 l1,l2의 값을 확인하라.

예제3에서 이어서 아래를 실행하라. 아래코드의 결과를 (l1,l2의 값, 메모리상황) 예측하라. 결과가 나오는 이유를 설명하라. 결과가 예제1과 다른 이유는?

l1[1].append(33)
l1,l2
([3, [66, 55, 44, 33]], [3, [66, 55, 44]])

- 예제5: 아래코드를 관찰하여 결과를 관찰하라. 결과가 예제1과 같은가 예제2와 같은가? -- 예제1과 같아요

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

a += [1] 는 새로운 오브젝트를 만드는게 아니고, 기존의 오브젝트를 변형하는 스타일의 코드구나? (마치 메소드처럼)

- 예제6: 아래코드를 실행하고 결과를 관찰하라. 왜 예제5와 다른가?

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

(해설)

  • -5,256까지의 값이 메모리 어딘가에 저장되어 있음.
  • 추가로 (66,55,44)이 저장될 공간이 필요함. 이 공간을 공간 X1이라 하자.

  • [3,공간x1의 값] 의 값이 저장될 공간이 필요함. 이 공간은 l1이라고 하자.

  • l2라는 공간을 만들어서 [3,공간x1의값]을 저장

  • 공간X1의 값에 33을 추가하고 싶음. 그런데 공간 X1에 저장된 값은 튜플인데 튜플은 값을 바꿀 수가 없다. 그래서 할수없이 새로운 공간 X2를 만들고 거기에 (66,55,44,33)을 넣고, 그것을 l2의 두번째 원소에 맵핑

정리하면 아래와 같은 상황임

  • 공간 X1: (66,55,44)
  • 공간 X2: (66,55,44,33)
  • 공간 l1: [3,공간X1의값]
  • 공간 l2: [3,공간X2의값] (기존에는 [3,공간X1의값]이었음)

기억할것

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

- 오묘하게도 [1000,2000,3000,4000,5000,6000]와 같이 0차원 자료형으로만 구성된 리스트라면 문제가 되지 않음. (메모리는 아끼면서 문제가 되지 않는다? 천재인데?)

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

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

deep copy

- 얕은복사가 아니라 내부객체까지 모두 새로 만드는 깊은 복사를 하기위해서는 아래와 같이 copy 모듈을 사용한다.

import copy 

- 깊은복사 예제1

l1 = [3,[66,55,44]] 
l2 = copy.deepcopy(l1)
id(l1),id(l2)
(139852030311616, 139851739502336)
id(l1[0]),id(l2[0])
(139852067748208, 139852067748208)
id(l1[1]),id(l2[1])
(139851739525504, 139851739117696)
l2[1].append(33)
l1
[3, [66, 55, 44]]
l2
[3, [66, 55, 44, 33]]

- 깊은복사 예제2

l1 = [3,[66,[55,44]]] 
l2 = copy.deepcopy(l1)
id(l1),id(l2) ## shallow copy = 1 level deep copy 
(139851739573248, 139851739217024)
id(l1[1]),id(l2[1]) ## (2 level) deep copy 
(139851740009024, 139851739501440)
id(l1[1][1]), id(l2[1][1]) ## (3 level) deep copy 
(139851738451072, 139851743243584)
l2[1][1].append(33)
l1,l2
([3, [66, [55, 44]]], [3, [66, [55, 44, 33]]])