일단 아래와 같은 리스트 초기화 예제는 많이 알고 있을 것이다.
[실험 코드]
size_x = 3
list = [0] * size_x
print(list)
[결과]
[0, 0, 0]
그러면 문득 위의 리스트 초기화 예제를 응용하다가 아래와 같은 잘못된 이차원 리스트 초기화를 시도할 수 있다.
[실험 코드]
size_x = 3
size_y = 3
list = [[0] * size_x] * size_y
print(list)
[결과]
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
결과만 보면 우리가 의도한 방식대로 출력되었기 때문에 문제가 있다고 생각하지 않을 수 있다. 그러나 위의 코드에서 특정 위치의 값을 변경해 보면 의도한 내용과 달리 결과가 이상함을 알 수 있다.
[실험 코드]
size_x = 3
size_y = 3
list = [[0] * size_x] * size_y
list[0][0] = 1
print(list)
[결과]
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
잠시 설명을 읽기 전에 왜 이렇게 결과가 나왔는지 진짜로 이렇게 결과가 나오는지 생각해 보자.
.
..
...
....
.....
......
.......
........
.........
잘 모르겠다면 아래 힌트 키워드를 참조하여 다시 한번 생각해 보자.
[힌트 키워드]
파이썬에서 함수 인자 전달 방식, Mutable, Immutable
충분히 생각을 해보았다면 내가 추론한 이유에 대하여 설명을 시작하도록 하겠다.
일단 초기 코드에서 [0] * size_x가 하는 일에 대해서 알아보자.
편의를 위해 간단한 코드로 초기화를 가능하게 하고 있지만 해당 코드가 호출될 때 초기화를 시켜주는 일정한 루틴이 호출되게 될 것이다. 결과를 가지고 해당 루틴에 대해서 유추해 보면 아래의 과정을 진행할 것이다.
- 앞의 list([0]) 안에 있는 값(0)을 size_x번만큼 복사하여 list에 추가한다.
그렇다면 [[0] * size_x] * size_y가 하는 일에 대해서 알아보자. [0]*size_x의 동작은 이미 알아보았기 때문에 해당 부분을 풀어서 [[0,0,0]] *size_y가 하는 일에 대해서 알아보자.
- 앞의 list([[0,0,0]]) 안에 있는 값([0,0,0])을 size_y번만큼 복사하여 list에 추가한다.
이 과정에서 의도한 다른 코드를 작성한 이유는 list의 안에 있는 값을 잘못 인식했기 때문이다. 해당 코드를 사용한 사람은 Immutable 한 [0, 0, 0]이라는 값이 존재할 것이라고 생각했을 했고 해당 값이 함수 전달 방식에서 call by value로 전달되어서 잘 처리되었을 것이라고 생각했을 것이다. 그런데 실제로 생각해 보면 [0, 0, 0]은 Mutable 한 list 객체이고 해당 객체가 전달되게 되면 call by reference로 전달되기 때문에 list의 결과로 동일한 list가 전달되어 저장되게 된다.
해당 설명이 어려운 사람은 파이썬의 함수 전달 방식에 대해서 좀 더 공부해 보면 쉽게 이해할 수 있을 것이다. 다시 한번 예제를 통해서 설명하면 정확히 동일한 동작을 하는 코드는 아니지만 아래와 같은 코드를 동작시킨 것으로 생각하면 된다.
[실험 코드]
mutable_list_object = [0, 0, 0]
list = []
list.append(mutable_list_object)
list.append(mutable_list_object)
list.append(mutable_list_object)
list[0][0] = 1
print(list)
[결과]
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]