포스트

[python] 리스트(List)의 복사 - 얕은 복사, 깊은 복사

List는 다양한 방법으로 복사가 가능하다.

다만, 필요 시에 따라 복사 방법을 적절히 택해야 하는데,

복사한 객체가 내부적으로 같은 메모리를 참조할 수 있기 때문이다.

이번 포스팅은 내부에 또 다른 복합자료형이 없는 1차원 배열에 국한된 이야기다.

2차원 이상의 배열에 대한 정보는 이 포스트에서 확인할 수 있다.


완전히 같은 메모리를 공유하게 만드는 복사(사실 아님)

보통 이 방법은 실수일 확률이 높다.

완전히 똑같은 메모리를 공유하는 객체를 생성한 것은(사실 생성한 것도 아니지만)

본체는 하나지만, 호출명을 하나 늘려줄 뿐이다.

그렇기 때문에 애초에 복사도 아니다.


1. 직접 할당 (copy_list = origin_list)

프로그래밍과 Python을 배우는 단계에서 많이 하는 실수다.

대부분의 경우, 직접 할당으로 탄생한 리스트 객체는 의도치 않게 동작한다.

1
2
3
origin_list = [1, 5, 10, 'hello', 'world']

copy_list = origin_list

아마 원래의 의도는 copy_list의 원소만 변경하고

origin_list와 비교하여 변경 사항을 체크하려는 용도를 생각할 것이다.

다만, 이 경우에는 copy_list의 변경사항이 origin_list에도 적용된다.

1
2
3
4
5
copy_list[0] = ''

print(origin_list)

# ['일', 5, 10, 'hello', 'world']

위의 방식은 사실상 origin_list에 copy_list라는 별명을 붙인 것과 같다.

새로운 객체를 생성한 것이 아니라, 객체를 호출할 이름(변수명)을 추가한 것이다.

두 리스트 객체의 메모리 값을 비교하면 똑같은 것을 알 수 있다.

1
2
3
4
5
6
7
print(id(copy_list) == id(origin_list))

# True

print(copy_list is origin_list)

# True

id(x) == id(y)x is y는 결과적으로 같은 연산을 수행한다.

==대신 is를 쓰지 말라고 하는 이유가 이것이다.
is는 객체의 값이 아닌, 객체가 참조하는 메모리 값을 비교하는 것이기 때문이다.


리스트는 다른 메모리, 내부 요소는 같은 메모리 (얕은 복사)

제목 그대로, 감싸고 있는 리스트는 새로운 객체지만 내부 요소는 같은 메모리에서 들고 온다.

이를 얕은 복사(shallow copy)라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
origin_list = [1, 5, 10, 'hello', 'world']

copy_list = origin_list.copy()

print(origin_list is copy_list)

# False

print(origin_list[3] is copy_list[3])

# True

copy_list[3] = 'bye'

print(origin_list)

# [1, 5, 10, 'hello', 'world'] / 직접 할당처럼 내부 요소의 값까지 바뀌진 않는다.

다시 강조하지만, List 내부에 복합자료형이 있는 2차원 이상의 배열의 경우 이야기가 달라진다

여기서의 설명은 어디까지나 list 내부에 str, int, bool 등의 기본 자료형만 있을 경우의 이야기다.

그런데,

내부 요소가 같은 메모리를 참조하면 변경사항이 공유되는 것 아니냐?

라는 물음이 생길 수 있다.

2차원 이상의 배열에서는 그럴 수 있으나, 1차원 배열일 때는 아니다.

같은 메모리를 참조한다는 것은 어디까지나 메모리의 효율성 증대를 위한 방법이다.

내부 요소의 값을 변경하는 작업은 메모리에 할당된 값을 바꾸는 게 아니다.

(만약 메모리에 할당된 값을 바꾼다면 실제로 변경 사항이 공유될 것이다.)

새로운 값을 새로운 메모리에 할당하고, 해당 메모리를 참조하도록 만드는 것이다.


1. list() 생성자 사용

Python을 보통 처음 배울 때 쓰는 방법인데, 나중에 가면 이런 게 있었지 하고 지나가는 방법이다.

아니면 map객체를 list로 바꿀 때나 쓰거나…

1
copy_list = list(origin_list)


2. list.copy() 메서드 사용

맨 위의 예시에서도 보여준 방법이다.

1
copy_list = origin_list.copy()


3. 슬라이싱 사용 list[:]

이게 직접할당이랑 뭐가 다르냐 싶겠지만

슬라이싱은 설정된 인덱스에 따라 새로운 리스트 객체를 생성하는 명령어다.

인덱스를 처음부터 끝까지[:]로 설정하면 그냥 내부 요소가 똑같은 새로운 리스트를 만들어준다.

1
copy_list = origin_list[:]

여담으로 다른 방법보다 짧고 활용 방법이 다양하다.


내부 요소까지 모두 새로운 메모리 할당 (깊은 복사)

위의 방법들은, list 자체는 새로이 생성하되,

내부 요소는 원본 리스트 내부 요소의 메모리를 참조하여 만든다.

이를 깊은 복사(deep copy)라고 한다.

하지만 이 방법은 내부 요소도 새로운 메모리에 할당하여 완전히 새로운 객체를 생성한다.

완전히 독립된 객체이므로, 두 리스트는 서로 어떠한 영향도 주지 않는다.


1. copy 모듈의 deepcopy 함수 활용

사실 잘 쓰지 않는 방법이고 쓸 필요도 보통 없다.

위에서 말했듯, 모든 값을 새로운 메모리에 할당하여

똑같은 값을 가지지만 메모리가 전혀 겹치지 않는 객체를 굳이 만드는 건

메모리 효율 면에서 매우 비효율적이다.

특히 내부에 복합자료형이 없는 1차원 배열이면 그냥 쓰면 안 된다.

1
2
3
4
5
6
7
8
9
from copy import deepcopy

origin_list = [1, 5, 10, 'hello', 'world']

copy_list = deepcopy(origin_list)

print(origin_list[0] is copy_list[0])

# False
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.