2차원 배열의 요소를 동일한 요소로 초기화 할 때 주의할 점

2차원 배열의 요소를 동일한 요소로 초기화 할 때 주의할 점

March 2, 2022

이번에 할 포스팅은 반성의 포스팅입니다 ㅠㅠ

이번에 코딩테스트에 응시하면서 했던 실수를 바로잡고자 하거든요!

문제 정의

바로 배열의 초기화와 요소 변경에 대한 문제입니다.

저는 2차원 배열을 동일한 요소로 파이썬 배열을 초기화 하는데 이런 식으로 초기화를 진행했습니다.

two_arr = [[False] * 2] * 5

이렇게 한 후 출력 결과를 살펴보면

[
  [False, False], 
  [False, False], 
  [False, False], 
  [False, False], 
  [False, False]
]

위와 같이 2*5 형태의 False 리스트 출력결과가 나오는 것을 볼 수 있습니다.

제가 의도한 대로 잘 초기화가 되었군요!

하지만 여기서 특정 배열의 요소를 바꾸어 보겠습니다.

two_arr[0][0] = True

저는 아래와 같이 첫번째 배열의 요소인 FalseTrue로 바뀌는 것을 기대했습니다.

[
  [True, False], 
  [False, False], 
  [False, False], 
  [False, False], 
  [False, False]
]

하지만 실제로 출력해보면 이런 결과가 나오는 것을 볼 수 있습니다.

[
  [True, False], 
  [True, False], 
  [True, False], 
  [True, False], 
  [True, False]
]

분명 [0][0]을 변경했는데 각 요소의 0번째 요소가 같은 영향을 받았군요. 무슨 일일까요?

Solution

문제는 동일한 요소를 갖는 파이썬 배열을 선언할 때의 곱셈연산(*)에 있습니다.

곱셈연산을 통해 특정 요소를 복사할 때 1차원 배열, 즉 [False] * 2와 같은 경우, False 라는 값을 2번 복사하여 리스트에 넣으라는 것이기 때문에 정상적으로 복사가 됩니다.

복사하는 대상이 리스트가 아닌 False라는 값이기 때문이죠. 결과로는 [False, False]과 같은 리스트가 형성됩니다.

다만 문제는 두번째에서 발생합니다. [[False] * 2] * 5 아래의 코드와 같이 선언된 배열([False * 2])을 다시 복사하는 과정에서 문제가 생깁니다.

[[False] * 2]와 같은 리스트 인스턴스는 이 아니라 주소이기 때문이죠.

이해가 되시나요? 결국 저는 [False, False] 리스트의 주소를 5번 복사하라는 명령을 한 것과 같습니다.

더욱 쉽게 말하면 [False, False]가 담긴 주소를 5번 복사한 것이죠. 따라서 각 열의 주소는 요소를 각각 공유하고 있기 때문에 특정 줄의 한 부분만 바꿔도 전체 줄의 한 부분이 바뀐 것처럼 행동하게 됩니다.

그래서 실제로 의도한 바와 같이 배열을 초기화하기 위해서는 for문을 통해 배열을 직접 초기화해주어야 합니다. for문을 돌며 [False, False]가 호출되는 시점이 모두 다르기에 주소가 각각 다르게 할당되기 떄문입니다.

이를 해결하기 위해서는 이렇게 append() 함수를 통해서 추가해주거나,

two_arr = []
for _ in range(5):
  two_arr.append([False] * 2)

좀 더 코드를 깔끔하게 쓰고 싶다면 아래와 같이 리스트 컴프리핸션(list_comprehension)을 사용하여 코드를 깔끔히 할 수 있습니다.

two_arr= [[False] * 2 for _ in range(5)]

이후 출력값을 비교해보면 의도한 대로 잘 나오는 것을 볼 수 있습니다.

결과값

[
    [True, False], 
    [False, False], 
    [False, False], 
    [False, False], 
    [False, False]
]

마치며

  • 이번 코딩테스트를 통해 내가 알고 있는 것이라도 다시 한번 크로스체크하는 것이 중요하다는 생각이 들었어요. 중요한 로직은 전부 구현은 했는데, 이런 곳에서 에러가 있을 줄이야.. :(
  • 리스트 변수는 객체이기 때문에 primitive type 변수와 다르게 주소값을 갖고 있다는 점. 포스팅을 보고 계시다면 여러분도 꼭 기억하셔서 저와 같은 실수를 하지 않으시길 바랍니다!