포스트

[python] 2차원 이상의 리스트(배열) 복사하기

2차원 이상의 리스트 복사하기

내부의 요소가 모드 기본 자료형인 1차원 배열과 달리,

2차원 배열부터는 리스트를 복사할 때 의도치 않은 문제가 발생하곤 한다.

보통 알고리즘 문제를 풀 때 그러한데,

BFS 문제로 visited를 만들 때기존 리스트를 복사할 때 문제가 발생한다.

먼저 기존의 리스트와 똑같은 요소를 갖고 있지만,

변경 사항이 전혀 공유되지 않는 독립된 리스트 만들기를 알아보자.


값은 똑같지만 완전히 독립된 객체 생성(deep copy)

이는 내부의 모든 요소를 새로운 메모리에 할당하여 새 객체를 만드는 것을 의미한다.

copy 모듈의 deepcopy 함수를 쓰자.

당연히 메모리 효율이 굉장히 나쁘지만, 이보다 확실하고 간단한 방법은 없는 것 같다.

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

origin_list = [[1, 2], [2, 4], [4, 6]]

copy_list = deepcopy(origin_list)

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

# False

다른 거의 모든 방법에서 가장 바깥 리스트를 제외한 내부의 요소들의 메모리 주소는 똑같이 설정된다.

바로 이 문제 때문에 2차원 이상의 배열에서는 바뀐 값이 공유되는 문제가 벌어진다.

그러나 deepcopy를 사용할 경우, 내부의 모든 요소들이 새로운 메모리에 할당된다.

따라서, 리스트 객체 자체는 물론 모든 요소들이 원본 리스트 및 그 내부 요소와 완전 독립되어 있다.

한 개의 리스트에서 값을 바꾸고 원본과 비교하는 방법을 쓸 것이라면, 이를 활용할 수 있으나,

메모리 효율이나 실행 속도 면에서 더욱 좋은 방법을 반드시 찾을 수 있을 것이다.

그래서 알고리즘 문제 풀이에서 보통 이 방법을 쓰는 사람은 본 적이 없다…


deepcopy 이외의 대부분의 경우…(얕은 복사를 쓸 경우)

list생성자(list(arr))

얕은 복사 메서드(list.copy())

슬라이싱 객체 대입(list[:])

컴프리헨서([i for i in ~~])

등등의 여러 방법들이 2차원 이상의 배열에서는 의도치 않은 문제가 발생한다.

1
2
3
4
5
6
7
8
9
A = [[1, 2], ['X', 'Y']]

B = A.copy()

A[1][0] = 'Z'

print(B[1][0])

# Z

위와 같이 원본 리스트에서 2depth 이상 내려간 요소를 변경할 경우,

해당 변경사항이 원본 객체 뿐만 아니라 복사한 객체에도 적용된다.

deepcopy를 제외한 다른 방법들은 가장 바깥의 리스트 객체만 새로 만든다(새 메모리 할당)

즉, 복사한 리스트 내부의 요소들도 원본 리스트 요소들의 메모리를 똑같이 참조한다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A = [[1, 2], ['X', 'Y']]

B = A.copy()

print(A is B)

# False

print(A[0] is B[0])

# True

print(A[0][1] is B[0][1])

# True

이처럼 말이다.

즉, A나 B나 내부 리스트와 요소들이 같은 메모리를 참조한다는 것인데

아예 그냥 메모리 상으로 동일한 객체라는 것이다.

A와 B가 똑같이 생긴 물건을 가지고 있는 게 아니라,

대여점에 하나 뿐인 물건을 번갈아 가져오는 것이라 생각하면 된다.

A가 책에 코딱지를 붙여 놓으면, 당연히 이후에 빌린 B가 본 책에는 코딱지가 붙어있다.

그렇기 때문에 2차원 이상의 배열을 원본과 완전히 독립된 객체로 만들기 위해서는

내부를 완전히 풀어헤치는 컴프리헨션을 쓰거나 해야 한다.

그 마저도, depth가 명확하지 않다면 재귀로 내려가야 하는 방법이다.

그러니 편하게 deepcopy를 쓰자.


visited 배열을 만들 때 하는 실수들

1
2
3
N = 10

visited = [[0] * N] * 10

이 경우에는 위의 경우들과 조금 다른 결의 문제가 발생한다.

1
2
3
4
5
visited[0][1] = 1

print(visited[5][1])

# 1       / ?????

이는 리스트 내부의 0번 부터 9번까지의 리스트가 똑같은 메모리를 참조하는 문제다.

즉, 0번째 리스트의 1번째 값을 1로 바꿨으나, 실제로는 2차원 배열의 1열 전체가 1로 바뀌는 것이다.

[0] * N 으로 생성된 리스트 객체가 c8이라는 메모리를 가졌다고 하면,

visited 리스트 내부는 [c8, c8, c8, c8, c8…] 이렇게 되어있다는 것이다…


visited 제대로 만들려면

컴프리헨션을 쓰자.

1
visited = [[0] * N for _ in range(N)]
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.