[Python] 리스트의 얕은 복사와 깊은 복사

Woody의 기록·2023년 12월 5일
2

Python

목록 보기
4/4

리스트의 얕은 복사와 깊은 복사

파이썬에서 리스트를 복사할 때, 얕은 복사깊은 복사라는 개념이 있다.

  • 얕은 복사(Shallow Copy)는 복사하려는 객체의 최상위 컨테이너만 복사하고 내부에 nested 되어 있는 객체는 공유하는 방식이다.
  • 깊은 복사(Deep Copy)는 복사하려는 객체의 최상의 컨테이너뿐만 아니라 내부에 nested 되어 있는 객체들까지도 복사하는 방식이다.

우리가 일반적으로 사용하는 복사한다는 의미는 동일한 값을 가지면서 완벽히 독립된 또 다른 대상을 만드는

것이므로 깊은 복사(Deep Copy)에 해당한다고 볼 수 있다.

얕은 복사와 깊은 복사는 어떻게 이루어지는가?

📌 얕은 복사(Shallow Copy)

얕은 복사는 최상위 컨테이너만 복사하고, 내부 객체들은 공유하는 복사 방식이다.

아래는 얕은 복사가 이루어지는 예시이다.

  1. 리스트의 전체 시퀀스를 슬라이싱(a.k.a. [ : ]) ****하는 경우
  2. copy 라이브러리(Lib/copy.py)의 copy 메서드(copy.copy)를 이용한 복사

🔘 얕은 복사 방식

→ 리스트 전체 시퀀스 슬라이싱( [ : ] )을 통한 얕은 복사

list_a = [1,2,3,4,5]
list_b = list_a[:]

list_a[2] = 99

print("list_a:", end = " ")
print(*list_a)

print("list_b:", end = " ")
print(*list_b)

'''
list_a: 1 2 99 4 5
list_b: 1 2 3 4 5
'''

→ copy.copy를 통한 얕은 복사

from copy import copy

list_a = [1,2,3,4,5]
list_b = copy(list_a)

list_a[2] = 99

print("list_a:", end = " ")
print(*list_a)

print("list_b:", end = " ")
print(*list_b)

'''
list_a: 1 2 99 4 5
list_b: 1 2 3 4 5
'''

🔘 shallow copy는 어떤 상황에서 적합한가?

결론부터 말하면 shallow copy는 복사하려는 대상의 내부에 mutable 객체들이 nested 되어있는 구조가

아니면 사용해도 된다.

이것을 이해하기 위해서 copy 대상의 내부 요소들에 따라서 shallow copy가 어떤 영향을 미치는지 살펴보자.

아래 세가지 경우로 나누어 살펴볼 것이다.

  • 내부에 객체가 nested 되어있지 않은 경우의 Shallow Copy
  • mutable 객체가 nested 되어있는 상태에서의 Shallow Copy
  • immutable 객체가 nested 되어있는 상태에서의 Shallow Copy

→ 내부에 객체가 nested 되어있지 않은 경우의 Shallow Copy

내부에 객체가 중첩되어 있지 않은 경우의 shallow copy

내부에 객체가 중첩되어 있지 않은 경우의 예시로 int 타입을 원소로 하는 1차원 리스트를 살펴보자

우선 리스트 슬라이싱을 이용하여 1차원 리스트를 얕은 복사한다.

복사하려는 대상의 최상위 컨테이너가 곧 1차원 리스트 자체가 되므로, 동일한 원소 값을 가지는 서로 다른 리스트가 생성된다.

list_a = [1,2,3,4,5]
list_b = list_a[:]

list_a[2] = 99

print("list_a:", end = " ")
print(*list_a)

print("list_b:", end = " ")
print(*list_b)

'''
list_a: 1 2 99 4 5
list_b: 1 2 3 4 5
'''

복사 후 list_a[2]의 값을 99로 변경해주어도 list_a와 list_b가 참조하는 1차원 리스트가 서로 다른 리스트이기

때문에 변경에 독립적이다.

→ mutable 객체가 nested 되어있는 상태에서의 Shallow Copy

mutable 객체가 nested 되어있는 케이스는 대표적으로 2차원 리스트를 예시로 들 수 있다.

2차원 리스트는 내부의 각 원소가 1차원 리스트 객체인 리스트로

최상위 컨테이너 내부에 mutable 객체인 리스트 객체가 nested 되어있는 예시로 볼 수 있다.

2차원 리스트를 리스트 슬라이싱 또는 copy로 shallow copy 하는 경우,

가장 최상 컨테이너는 독립적으로 복사되므로 각 리스트는 독립적인 메모리에 저장된다.

list_a와 list_b의 id는 각각 139898283870720, 139898283870848 이므로 서로 다른 것을 확인해볼 수 있다.

하지만 얕은 복사는 내부 객체의 참조에 대해서는 복사하지 않고 공유하게 된다.

따라서 shallow copy는 1차원 리스트처럼 리스트 내에 또 리스트등의 객체가 중첩되지 않은 경우에만

사용가능한 복사 방법이라고 볼 수 있다.

예를 들어 shallow copy 후에 list_a[0][2]를 99로 할당한 경우,

list_b의 내부 객체인 list_b[0]와 list_a[0]은 동일한 참조를 가지므로(공유하므로) list_b[0][2]도 변경된 값을 바라보게 된다.

얕은 복사는 내부 객체는 복사되지 않고 공유된다

list_a = [[1,2,3,4,5],[6,7,8,9,10]]
list_b = list_a[:] # copy.copy(list_a)도 동일하게 shallow copy가 일어난다.

list_a[0][2] = 99

print("list_a({}):".format(id(list_a)), end = " ")
print(*list_a)

print("list_b({}):".format(id(list_b)), end = " ")
print(*list_b)

'''
list_a(139898283870720): [1, 2, 99, 4, 5] [6, 7, 8, 9, 10]
list_b(139898283870848): [1, 2, 99, 4, 5] [6, 7, 8, 9, 10]
'''

list_a와 list_b의 내부 객체 비교

내부 객체는 동일한 참조를 가지는 것을 볼 수 있다.

print("list_a[0]: {}",format(id(list_a[0])))
print("list_b[0]: {}",format(id(list_b[0])))

'''
list_a[0]: {} 139673405917824
list_b[0]: {} 139673405917824
'''

→ immutable 객체가 nested 되어있는 상태에서의 Shallow Copy

이번에는 immutable 객체가 nested 되어있는 상태에서의 shallow copy 예시를 들어보았다.

튜플은 immutable한 객체로, 한번 초기화되어 메모리에 올라가면 값을 변경할 수 없다.

만약 초기화된 튜플을 재할당한다면 어떻게 될까?

  • list_a[0]를 (1,2)로 초기화 한 후 list_a를 shallow copy 한 결과를 list_b에 넣어준다.
  • 이후 list_a[0] = (9, 9)를 수행하도록 하여 (1, 2)가 있는 자리를 (9, 9)로 바꿔주었다.

immutable 객체가 nested 되어있는 상태에서의 shallow copy 예시

list_a = [(1,2), (3,4)]
list_b = list_a[:]

list_a[0] = (9,9)

print("list_a({}):".format(id(list_a)), end = " ")
print(*list_a)

print("list_b({}):".format(id(list_b)), end = " ")
print(*list_b)

print("list_a[0]: {}",format(id(list_a[0])))
print("list_b[0]: {}",format(id(list_b[0])))

'''
list_a(140089982632960): (9, 9) (3, 4)
list_b(140089982632768): (1, 2) (3, 4)

id(list_a[0]): 140089982633216
id(list_b[0]): 140089982633088

id(list_a[1]): 140089982633152
id(list_b[1]): 140089982633152
'''

이렇게 되면 기존에 (1,2)가 저장되어 있는 위치인 140089982633088에 저장된 리터럴값을

9, 9로 수정하는 것이 아니라 (9, 9) 튜플을 메모리의 새로운 위치(현재 예시에서는 140089982633216)에

저장한 후, 해당 참조를 list_a[0]가 가지게 된다.

list_b[0]은 여전히 140089982633088를 가리키므로 튜플 (1, 2)를 바라본다.

따라서 내부 객체가 immutable 객체라면 shallow copy를 사용해서 내부 객체가 공유된다고 해도,

내부 상태 변화에 대해 두 리스트가 독립적임으로 동작하는 것이 보장된다.

따라서 list_a와 list_b 모두 내부 객체(튜플)를 복사 초기에 공유하였다라도 내부 객체를 변경할 수 없고 재할당만

가능한 immutable 객체에서는 shallow copy를 해도 무관하다.

📌 깊은 복사(Deep Copy)

깊은 복사는 최상위 컨테이너뿐만 아니라 내부 객체들도 모두 복사하는 방식이다.

내부에 mutable 객체가 nested 되어 있어도 내부까지 완벽히 복사된다. 즉, 내부에 공유되는 부분없이 완전히

독립적인 객체로 복사 할 수 있다.

🔘 깊은 복사 방식

아래는 깊은 복사가 이루어지는 방식이다.

  1. 리스트 슬라이싱을 사용한 깊은 복사

    → 복사하려는 리스트의 크기가 커질수록 copy.deepcopy에 비해 수행시간 측면에서 더 좋은 성능을 보여준다.

  1. copy 라이브러리(Lib/copy.py)의 deepcopy 메서드(copy.deepcopy)를 이용한 복사

    → 가장 직관적이고 구현이 간편하다. 하지만 copy.deepcopy 메서드는 리스트 크기가 커질수록 수행시간 측면에서 좋지 않은 성능을 보여준다.

→ 리스트 슬라이싱(List Slicing)을 통한 깊은 복사

리스트 슬라이싱을 중첩된 내부 객체들의 최소 단위로 적용해주면 깊은 복사를 구현할 수 있다.

중첩된 객체들의 최소 단위에 대한 슬라이싱 해주면, 더 이상 내부에 mutable 객체의 중첩이 없는 상태인 단위로 슬라이싱 하는 것이기 때문에 깊은 복사가 된다.

예를 들어 2차원 리스트를 복사하는 예시이다.

리스트 슬라이싱을 이용한 2차원 리스트 깊은 복사

from copy import copy

list_a = [[1,2,3,4,5], [6,7,8,9,10]]

list_b = [row[:] for row in list_a]
    

list_a[1][1]= 200

print("list_a({}):".format(id(list_a)), end = " ")
print(*list_a)

print("list_b({}):".format(id(list_b)), end = " ")
print(*list_b)
print("")

print("id(list_a[0]): {}".format(id(list_a[0])))
print("id(list_b[0]): {}".format(id(list_b[0])))
print("")

print("id(list_a[1]): {}".format(id(list_a[1])))
print("id(list_b[1]): {}".format(id(list_b[1])))

'''
list_a(140065618740352): [1, 2, 3, 4, 5] [6, 200, 8, 9, 10]
list_b(140065618741248): [1, 2, 3, 4, 5] [6, 7, 8, 9, 10]

id(list_a[0]): 140065618740288
id(list_b[0]): 140065618740800

id(list_a[1]): 140065618740096
id(list_b[1]): 140065618745088
'''

현재 예시에서 중첩된 객체들의 가장 최소 단위인 1차원 리스트(row) 단위로 슬라이싱([:])을

수행해주었기 때문에 list_a[i]와 list_b[i]의 참조가 모두 달라지게 되면서 깊은 복사가 이루어지게 된다.

실제로 list_a와 list_b의 각 1차원 리스트에 대해 참조를 확인해보아도 다른 것을 확인할 수 있다.

따라서 list_a의 특정값을 변경해도 list_b는 해당 변경에 독립적이게 된다.

그럼 3차원 리스트에서는 어떨까?

리스트들이 중첩되어 이루어진 3차원 리스트에서도 마찬가지로 가장 작은 단위는 1차원 리스트이다.

2차원 리스트에서와 마찬가지로 더이상 내부에 객체들이 nested 되지 않은 단위로

슬라이싱 해주면 deepcopy가 된다.

리스트 슬라이싱을 이용한 3차원 리스트 깊은 복사

list_a = [
    [[1,2,3], 
     [4,5,6],
     [7,8,9]],
    [[11,22,33],
     [44,55,66],
     [77,88,99]]
]

list_b = [[row[:] for row in height] for height in list_a]
    

list_a[1][1][1]= 200

print("list_a({}):".format(id(list_a)), end = " ")
print(*list_a)

print("list_b({}):".format(id(list_b)), end = " ")
print(*list_b)
print("")

print("id(list_a[1]): {}".format(id(list_a[1])))
print("id(list_b[1]): {}".format(id(list_b[1])))

print("id(list_a[0][0]): {}".format(id(list_a[0][0])))
print("id(list_b[0][0]): {}".format(id(list_b[0][0])))
print("")

'''
list_a(139683193463232): [[1, 2, 3], [4, 5, 6], [7, 8, 9]] [[11, 22, 33], [44, 200, 66], [77, 88, 99]]
list_b(139683193467264): [[1, 2, 3], [4, 5, 6], [7, 8, 9]] [[11, 22, 33], [44, 55, 66], [77, 88, 99]]

id(list_a[1]): 139683193460992
id(list_b[1]): 139683193620800
id(list_a[0][0]): 139683193455744
id(list_b[0][0]): 139683193620736
'''

깊은 복사가 제대로 이루어졌는지 id를 확인해보면 3차원 리스트 내의 원소인 2차원 리스트들도

전부 다른 참조를 가지고 있고, 또 그 2차원 리스트 내부의 원소들인 1차원 리스트들도

전부 다른 참조를 가지고 있는 것을 볼 수 있다.

리스트 컴프리헨션에 익숙하지 않다면 조금은 복잡해 보일 수 있으나,

copy.deepcopy를 사용하는 것보다 수행 시간 성능이 좋다는 장점이 있다.

→ copy.deepcopy를 이용한 깊은 복사

copy.deepcopy는 이미 구현된 메서드를 사용하는 것이기 때문에,

별도로 확인하지 않아도 깊은 복사가 보장된다.

또한 리스트 슬라이싱으로 구현하는 것에 비해 코드가 간결하다는 장점이 있다.

deepcopy를 이용한 3차원 리스트 깊은 복사

from copy import deepcopy

list_a = [
    [[1,2,3], 
     [4,5,6],
     [7,8,9]],
    [[11,22,33],
     [44,55,66],
     [77,88,99]]
]

list_b = deepcopy(list_a)
    

list_a[1][1][1]= 200

print("list_a({}):".format(id(list_a)), end = " ")
print(*list_a)

print("list_b({}):".format(id(list_b)), end = " ")
print(*list_b)
print("")

print("id(list_a[1]): {}".format(id(list_a[1])))
print("id(list_b[1]): {}".format(id(list_b[1])))

print("id(list_a[0][0]): {}".format(id(list_a[0][0])))
print("id(list_b[0][0]): {}".format(id(list_b[0][0])))
print("")

당연한 이야기지만 굳이 id를 확인해보면 3차원 리스트 내의 원소인 2차원 리스트들도

전부 다른 참조를 가지고, 또 그 2차원 리스트 내부의 원소들인 1차원 리스트들도

전부 다른 참조를 가지고 있는 것을 확인할 수 있다.

하지만 deepcopy는 복사하려는 리스트의 크기가 커질수록 수행 시간 성능이 떨어진다는 것을 고려해야한다.

📌 할당은 복사가 아니다

대입 연산자를 통한 객체 할당

객체를 복사한다는 의도로, 복사하려는 대상과는 독립적인 메모리 공간에 복사하려는 대상의 값이

복사되기를 바라며 대입연산자(=)로 대입하는 경우가 있다.

대입연산자는 단지 동일한 참조를 할당할 뿐 복사가 아님에 주의해야한다.

list_a = [1,2,3,4,5]
list_b = list_a # list_a와 list_b는 메모리상 동일한 지점을 참조한다.

print("id(list_a):{}\n\
id(list_b): {}".format(id(list_a), id(list_b)))

'''
id(list_a):139908937134464
id(list_b): 139908937134464
'''

따라서 list_a에 변경이 일어나면 동일한 메모리 공간을 참조하는 list_b도 변경된 결과를 바라보게 된다.

list_a = [1,2,3,4,5]
list_b = list_a # list_a와 list_b는 메모리상 동일한 지점을 참조한다.

list_a[2] = 99

print("list_a:", end = " ")
print(*list_a)

print("list_b:", end = " ")
print(*list_b)

'''
list_a: 1 2 99 4 5
list_b: 1 2 99 4 5
'''
profile
Github - https://www.github.com/woody35545

0개의 댓글