파이썬 - 다차원 배열의 복사

mauz·2022년 4월 30일
0

TIL - Today I Learned

목록 보기
8/10

https://www.acmicpc.net/board/view/88897#comment-143471

BFS 유형 문제를 풀다가 2차원 배열을 복사하고 싶어서 list 함수를 이용했더니 결과가 이상하더라,
알고보니 복사된 배열의 값을 바꾸면 원본 배열의 값도 따라서 바뀌었기 때문이었다.

다음과 비슷한 상황이 나타났다.

matrix = [[1,2,3],[1,2,3],[1,2,3]]

submatrix = list(matrix)

submatrix[0][0] = 5

print(submatrix)
#[[5,2,3],[1,2,3],[1,2,3]]
print(matrix)
#[[5,2,3],[1,2,3],[1,2,3]]

항상 list함수로 배열을 복사하던 나로서는 이해 할 수 없어서 질문글을 게시했더니 친절하신분이 장문으로 답변을 달아주셨다.


파이썬에는 자료형에 따라 immutable mutable 특성이 있음을 알고 있어야한다.

수정 불가능한 객체를 immutable 객체라고 부르고 수정 가능한 객체를 mutable 객체라고 부른다.

class설명구분
listmutable한 순서가 있는 객체집합mutable
setmutable 한 순서가 없는 고유한 객체 집합mutable
dictkey와 value가 맵핑된 객체, 순서 없음mutable
bool참, 거짓immutable
int정수immutable
float실수immutable
tupleimmutable한 순서가 있는 객체immutable
str문자열immutable
frozensetimmutalbe한 setimmutable

https://wikidocs.net/16038

a = 1
b = 2
a = b

print(a)
# 2

a는 1을 가리키다가 2를 가리키기 시작했다.
a = b를 하기 전과 후의 a는 완전히 다른 값이다.

arr = []
arr.append(1)

print(arr)
# [1]

arr는 빈 리스트였지만 1이 추가되었다. 그러나 1이 추가되기 전의 arr과 1이 추가된 후의 arr은 같은 리스트이다. 내용이 달라진다고 새로운 리스트가 되는게 아니다.


리스트의 복사

리스트 복사에 앞서서 흔히 정수 자료형의 값을 복사하고 값을 수정할땐

a = 1
b = a
b = 5

print(a, b)
# 1, 5

위와 같은 방법으로 원본 a의 값을 유지한채 b의 값을 바꿀수있다.
같은 방법으로 list 자료형을 복사해보자.

a = []
b = a
for i in range(1,4):
	b.append(i)

print(a, b)
# [1,2,3], [1,2,3]

이번엔 b에 원소를 추가했지만 a에도 원소가 추가되었음을 볼 수 있다.

왜 이런 결과가 나타날까?

정수형을 복사할때는

a = 1 
 # a 는 메모리 공간 상에서 정수 1이 존재하는 곳을 가리킨다.
b = a 
 # b는 변수 a가 바라보는 정수 1이 존재하는 곳을 가리킨다.
b = 5 
 # b는 메모리 공간 상에서 정수 5가 존재하는 곳을 가리킨다.

print(a, b)
# 1, 5

a는 1이 존재하는 곳을 가리키고, b는 5가 존재하는 곳을 가리킨다.

리스트를 (잘못)복사했을때는

a = [1,2,3] 
 # 메모리 공간 상에 새로운 리스트 [1,2,3]를 생성하고 그것을 가리키는 변수 a
b = a 
# b는 변수 a가 가리키는 [1,2,3]를 가리킨다.
# a와 b는 메모리 상에 있는 같은 [1,2,3]를 가리키고 있다.

b.append(5)
 # b가 가리키는 리스트[1,2,3]에 5를 추가한다. 

print(a, b)
print(a is b)
# [1,2,3,5], [1,2,3,5]
# True

a와 b가 가리키는 리스트가 같기 때문에,
a와 b를 출력했을때 나타나는 값이 [1,2,3,5]으로 같고,
a is b 를 출력했을때 동일한 객체임이 맞으므로 True가 나타난다.


얕은 복사(swallow copy)

그럼 리스트를 복사하는 적절한 방법은 뭘까?

a와 b가 같은 리스트를 가리키는 것이 문제였으므로
서로 다른 리스트를 가리키게하면 될 것이다.

a = [1,2,3]
b = []	# 새로운 리스트를 메모리 공간에 생성한다.

for i in a:		# a리스트에 담긴 요소들을 하나씩 b에 추가한다.
    b.append(i)

print(a,b)
print(a is b)
# [1,2,3],[1,2,3]
# False
b.append(5)
print(a,b)
# [1,2,3],[1,2,3,5]

이렇게 하면 a와 동일한 요소들을 담고있지만 서로 다른 리스트인 b를 만들 수 있고,
b에 담긴 값이 변해도 a에 영향을 주지 못한다.

b = list(a)
b = a.copy()
b = a[:]

위와 같은 방법으로 간단하게 a에 담긴 요소들을 복사한 리스트를 만들어 낼 수도 있다. 그리고 이런 방법을 '얕은 복사'라고 한다.


아하 그럼 2차원 배열도 똑같이 하면 되겠네!!

인줄 알았으나 아니었다..

3x3 짜리 2차원 배열을 복사하기 위해 list함수를 사용했는데..

matrix = [[1,2,3],[1,2,3],[1,2,3]]

submatrix = list(matrix)

submatrix[0][0] = 5

print(submatrix)
print(matrix)
# [[5,2,3],[1,2,3],[1,2,3]]
# [[5,2,3],[1,2,3],[1,2,3]]

print(submatrix is matrix)
print(submatrix[0] is matrix[0])
# False
# True

matrix와 submatrix는 서로 다른 리스트임이 확인된다.
그러나 matrix의 하위 리스트와 submatrix의 하위 리스트는 동일한 객체인 것으로 나타났다!

조악하게나마 그린 그림으로 구조를 살펴보면 아래와 같다.

하위리스트가 서로 같은 리스트인 이유는 '얕은 복사'로 복사를 진행하는 과정이

  1. 비어있는 리스트를 메모리 상 공간에 생성하고
  2. 원본 리스트에 들어있는 요소들을 하나씩 비어있는 리스트에 추가한다.

여기서 2차원 배열의 요소는 1차원 배열이다.
다시말해서 list를 추가해준 것인데, 새로운 리스트를 만들어 list속 요소들을 추가하여 만든 list가 아닌 원본의 리스트를 그냥 추가해준 것이다!!

이 때문에 복사본의 하위리스트가 원본의 리스트와 동일한 객체가 되었고, 복사본의 값을 변경하면 원본의 값도 변하는 듯한 모습이 나타났던 것이다.


깊은 복사(deep copy)

해결방법은 얕은복사의 방법처럼 새로운 리스트를 만들고 원본리스트의 값들을 하나씩 추가하는 것이다.

matrix = [[1,2,3],[1,2,3],[1,2,3]]

submatrix = []

for arr in matrix:
    submatrix.append([])
    for i in arr:
        submatrix[-1].append(i)

submatrix[0][0] = 5
print(matrix)
print(submatrix)
# [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
# [[5, 2, 3], [1, 2, 3], [1, 2, 3]]

이중 for문으로 이차원 배열의 복사를 구현할 수 있다.

다른 방법으로는 내장 라이브러리인 copy의 deepcopy 모듈을 이용하면 보다 간단히 표현할 수 있다.

from copy import deepcopy

matrix = [[1,2,3],[1,2,3],[1,2,3]]

submatrix = deepcopy(matrix)

submatrix[0][0] = 5
print(matrix)
print(submatrix)

또한 인덱스 슬라이싱을 이용하여 나타낼 수 있는데 deepcopy보다 빠르게 동작한다고 한다.
출처

matrix = [[1,2,3],[1,2,3],[1,2,3]]

submatrix = [i[:]for i in matrix]

submatrix[0][0] = 5
print(matrix)
print(submatrix)

후기

다른 사람들의 코드를 들여다보다가 deepcopy를 사용하던 것이 보였으나 어디다 써먹는거지 싶었는데 이번에 궁금증을 해결하면서 deepcopy의 쓰임새를 알게되었다.

아직도 모르는부분이 많이 남았고, 공부가 많이 필요하다고 느꼈다.

글을 쓰면서 인덱스 슬라이싱의 동작이 궁금해졌다..

백준에 올린 질문글에 친절히 답변을 달아주신 분에게 너무 감사하다🙏

profile
쥐구멍에 볕드는 날

0개의 댓글