[python] shallow copy, deep copy (얕은 복사, 깊은 복사)

- 10 mins

shallow copy & deep copy

파이썬에는 얕은 복사(shallow copy)와 깊은 복사(deep copy)라는 개념이 있다.

하나씩 알아보자.

shallow copy (얕은 복사)

다음과같이 숫자, 리스트, 튜플을 담고있는 l1이라는 리스트가 있다.

# -*- coding:utf-8 -*-

l1 = [
    12345,
    [1, 2, 3, 4, 5],
    (1, 2, 3, 4, 5)
]

l1을 복사해 l2라는 리스트를 만들고, 각 요소에 대해 is와 == 비교를 해보자.

is와 ==비교의 차이를 모른다면 따로 포스팅을 해두었으니 참고하면 될 것 같다.

간단히 설명하자면 is는 완전히 동일한 객체인지 비교, == 은 단순 값 비교이다.

  1 # -*- coding:utf-8 -*-
  2
  3 l1 = [
  4     12345,
  5     [1, 2, 3, 4, 5],
  6     (1, 2, 3, 4, 5)
  7 ]
  8
  9 l2 = list(l1)
 10
 11 print("l1 is l2 : {}".format(l1 is l2))
 12 print("l1 == l2 : {}".format(l1 == l2))
 13 print
 14 print("l1[0] is l2[0] : {}".format(l1[0] is l2[0]))
 15 print("l1[0] == l2[0] : {}".format(l1[0] == l2[0]))
 16 print
 17 print("l1[1] is l2[1] : {}".format(l1[1] is l2[1]))
 18 print("l1[1] == l2[1] : {}".format(l1[1] == l2[1]))
 19 print
 20 print("l1[2] is l2[2] : {}".format(l1[2] is l2[2]))
 21 print("l1[2] == l2[2] : {}".format(l1[2] == l2[2]))

3번째 줄에서 l1을 선언하고,

9번째 줄에서 l1을 복사해 l2를 만들었다.

그리고 11 ~ 21 줄에 걸쳐 각 요소르 값을 비교했다.

결과는 다음과 같다.

l1 is l2 : False
l1 == l2 : True

l1[0] is l2[0] : True
l1[0] == l2[0] : True

l1[1] is l2[1] : True
l1[1] == l2[1] : True

l1[2] is l2[2] : True
l1[2] == l2[2] : True

결과를 분석해보자.

l1와 l2를 통째로 is비교 했을때에는 False가 나왔다.

두 객체가 다른 주소값을 가진다는 의미이다.


하지만 각 요소를 비교 했을때에는 True가 나왔다.

각 요소는 같은 주소값을 가리킨다는 의미이다.

이게 왜 중요할까?

가리키는 주소값이 같다는 것은, l1의 값을 바꾸면 l2도 바뀐다는 의미이기 때문이다.

정말 그런지 확인해보자.

l1이 가지고있는 리스트에 값을 추가해보자.

  1 # -*- coding:utf-8 -*-
  2
  3 l1 = [
  4     12345,
  5     [1, 2, 3, 4, 5],
  6     (1, 2, 3, 4, 5)
  7 ]
  8
  9 l2 = list(l1)
 10
 11 l1[1].append(6)  # [1, 2, 3, 4, 5] 리스트에 6 추가
 12
 13 print(l1[1])  # [1, 2, 3, 4, 5, 6]
 14 print(l2[1])  # [1, 2, 3, 4, 5, 6]

11번째 줄에서 6을 추가했다.

13번째 줄에서 l1[1]을 출력해보니 정상적으로 추가가 되었다.

그런데 문제는, 14번째 줄에서 l2[1]을 출력해보니 이쪽에도 6이 추가되어버렸다.

그러면 이번엔 세 번째 요소인 튜플에 값을 추가해볼까?

  1 # -*- coding:utf-8 -*-
  2
  3 l1 = [
  4     12345,
  5     [1, 2, 3, 4, 5],
  6     (1, 2, 3, 4, 5)
  7 ]
  8
  9 l2 = list(l1)
 10
 11 l1[2] += (6, 7)
 12
 13 print(l1[2])  # (1, 2, 3, 4, 5, 6, 7)
 14 print(l2[2])  # (1, 2, 3, 4, 5)

똑같은 상황인데, 튜플은 정상적으로 l1만 변경되고 l2는 변경되지 않았다.

리스트와 무슨 차이일까?


리스트는 가변형 타입이고, 튜플은 불변형 타입이다.

즉, 리스트는 가리키고있는 주소값을 그대로 둔 채, 그 안의 값을 추가하거나 바꿀 수 있다.

반면 튜플은 기본적으로 값의 변경이 되지 않는 타입이기때문에, 새로운 값을 만들어서 주소값을 다시 할당한다.

우리가 느낄때는 값을 추가한다고 생각하지만, 실제로는 새로운 주소값이 생성되는 것이다.


즉, 리스트에 값을 추가할때는 값을 변경해도 주소값이 그대로 있으므로,

l1에서 값 변경시 l2의 입장에서도 가리키던 주소의 값이 바뀌기때문에 값이 같이 변경되어 버리는 것이다.


하지만 튜플의 경우에는 l1에서 값 변경시 아예 새로운 주소값을 만들게되므로,

l2가 가리키던 주소값에는 아무런 영향을 주지 않는 것이다.

정말 그런지 확인해볼까?

  1 # -*- coding:utf-8 -*-
  2
  3 l = [1, 2, 3, 4, 5]
  4 t = (1, 2, 3, 4, 5)
  5
  6 print("============값 추가 전================")
  7 print
  8 print("리스트 값 : {}".format(l))
  9 print("리스트 주소 : {}".format(id(l)))
 10 print
 11 print("튜플 값 : {}".format(t))
 12 print("튜플 주소 : {}".format(id(t)))
 13
 14 l += [6, 7]  # 리스트에 값 추가
 15 t += (6, 7)  # 튜플에 값 추가
 16
 17
 18 print
 19 print("============값 추가 후================")
 20 print
 21 print("리스트 값 : {}".format(l))
 22 print("리스트 주소 : {}".format(id(l)))
 23 print
 24 print("튜플 값 : {}".format(t))
 25 print("튜플 주소 : {}".format(id(t)))

3번 라인에서 리스트를 생성하고,

4번 라인에서 튜플을 생성했다.

그리고 6 ~ 12 라인에 걸쳐 리스트/튜플의 값과 주소를 출력했다.


14번 라인에서 리스트에 값을 추가하고,

15번 라인에서 튜플에 값을 추가했다.

그리고 19 ~ 25 라인에 걸쳐 변경된 리스트/튜플의 값과 주소를 출력했다.

결과를 보자

============값 추가 전================

리스트 값 : [1, 2, 3, 4, 5]
리스트 주소 : 139670775256776

튜플 값 : (1, 2, 3, 4, 5)
튜플 주소 : 139670775939440

============값 추가 후================

리스트 값 : [1, 2, 3, 4, 5, 6, 7]
리스트 주소 : 139670775256776

튜플 값 : (1, 2, 3, 4, 5, 6, 7)
튜플 주소 : 139670776110560

튜플과 리스트 둘다 값은 제대로 추가되었다.

하지만 주소를 보면 리스트는 139670775256776 그대로인 반면,

튜플을 139670775939440 에서 139670776110560 으로 변경되었다.


즉, 리스트는 주소값을 그대로 둔 채 값의 변경이 가능하다.

튜플은 주소값을 그대로 둔 채 값을 변경이 불가능하다.

따라서 리스트는 가변형 변수, 튜플은 불변형 변수라고 부른다.


shallow copy시 불변형 변수를 사용할 때에는 상관이 없지만,

가변형 변수를 사용한다면 문제가 된다.

가르키는 주소값이 똑같기때문에 값도 같이 변경되기 때문이다.

deep copy (깊은 복사)

그럼 위와 같은 상황을 방지하려면 어떻게 해야할까?

바로 deep copy를 사용하는 것이다.

copy라는 모듈의 deepcopy 메소드를 사용하면 된다.

똑같은 상황을 deepcopy로 복사해보자.

  1 # -*- coding:utf-8 -*-
  2
  3 from copy import deepcopy
  4
  5 l1 = [
  6     12345,
  7     [1, 2, 3, 4, 5],
  8     (1, 2, 3, 4, 5)
  9 ]
 10
 11 l2 = deepcopy(l1)
 12
 13 print("l1 is l2 : {}".format(l1 is l2))
 14 print("l1 == l2 : {}".format(l1 == l2))
 15 print
 16 print("l1[0] is l2[0] : {}".format(l1[0] is l2[0]))
 17 print("l1[0] == l2[0] : {}".format(l1[0] == l2[0]))
 18 print
 19 print("l1[1] is l2[1] : {}".format(l1[1] is l2[1]))
 20 print("l1[1] == l2[1] : {}".format(l1[1] == l2[1]))
 21 print
 22 print("l1[2] is l2[2] : {}".format(l1[2] is l2[2]))
 23 print("l1[2] == l2[2] : {}".format(l1[2] == l2[2]))

3번 라인에서 copy 모듈을 import했다.

그리고 맨 첫번째 예제에서 l2를 생성하는 부분만 deepcopy로 변경했다.

결과를 보자.

l1 is l2 : False
l1 == l2 : True

l1[0] is l2[0] : True
l1[0] == l2[0] : True

l1[1] is l2[1] : False
l1[1] == l2[1] : True

l1[2] is l2[2] : True
l1[2] == l2[2] : True

대부분 비슷한 결과인데, 다른부분이 보인다.

바로 list가 담겨있는 1번 요소에 대한 비교가 False로 나온것이다.

list는 가변형 인자이므로 카피본이나 원본을 변경할 경우 양쪽 모두 값이 변경되는 것을 확인했었다.

그것을 방지하기 위해 가변형 인자의 경우 처음부터 다른 주소값을 할당하는 것이다.

0번 요소인 integer 타입이나 2번 요소인 tuple 타입은 애초에 불변형 인자이므로 같은 주소값을 할당했다.

어차피 카피본이나 원본의 값이 바뀌어도 주소값 자체가 변경되기 때문에 서로에게 영향을 주지 않기 때문이다.

결론

shallow copy의 경우,

콜렉션 타입내에 가변형 타입이 있을 경우 위와 같이 예상치 못한 결과를 초래할 수 있으므로 주의해야한다.


그렇다고해서 이를 방지하기위해 무조건 deep copy를 사용하는것도 바람직하지 않다.

보다시피 deep copy는 콜렉션 객체가 가진 요소의 모든 타입을 체크한 후에,

가변형 타입일 경우 주소값을 다르게 할당하는 작업이 추가되어 더 느리기 때문이다.


콜렉션 타입이 가진 요소들을 잘 파악해서,

모든 요소가 불변형 타입일 경우에는 shallow copy를,

요소 중 가변형 타입이 포함되어있을 경우에는 deep copy를 사용하는것이 좋다.




코딩장이

코딩장이

-장이: [접사] ‘그것과 관련된 기술을 가진 사람’의 뜻을 더하는 접미사.

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora