2次元配列の要素を同じ要素で初期化する際の注意点

2次元配列の要素を同じ要素で初期化する際の注意点

2022年3月2日

今回のポスティングは反省のポスティングです ㅠㅠ

今回はコーディングテストに挑戦しながら犯したミスを正そうと思います!

問題定義

それは、配列の初期化と要素変更に関する問題です。

私は2次元配列を同じ要素でPythonで初期化する際に、このように初期化を行いました。

two_arr = [[False] * 2] * 5

このようにした後、出力結果を見ると

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

上記のように2*5形態のFalseリストの出力結果が出ることがわかります。

意図した通りにうまく初期化されましたね!

ですが、ここで特定の配列の要素を変えてみます。

two_arr[0][0] = True

私は以下のように1つ目の配列の要素だったFalseTrueに変わることを期待しました。

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

しかし、実際に出力してみるとこのような結果になることがわかります。

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

確かに[0][0]を変更したのに、各要素の0番目の要素が同じ影響を受けました。どういうことでしょうか?

解決策

問題は、同じ要素を持つPython配列を宣言する際の掛け算演算(*)にあります。

掛け算演算を通じて特定の要素をコピーする際、一次元配列、つまり[False] * 2のような場合、Falseという値を2回コピーしてリストに入れることなので正常にコピーされます。

コピーする対象がリストではなくFalseという値であるためです。結果として[False, False]のようなリストが形成されます。

ただし、問題は2つ目に発生します。[[False] * 2] * 5下のコードと同様に宣言された配列([False * 2])を再度コピーする過程で問題が発生します。

[[False] * 2]のようなリストインスタンスはではなくアドレスであるためです。

理解できますか?結局、私は[False, False]リストのアドレスを5回コピーしろという命令をしたのと同じです。

さらに言えば、[False, False]が入ったアドレスを5回コピーしたのです。したがって、各行のアドレスは要素をそれぞれ共有しているため、特定の行の一部分だけを変えても全体の行の一部分が変わったかのように振る舞うことになります。

そこで実際に意図したようにリストを初期化するためには、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]
]

終わりに

  • 今回のコーディングテストを通じて、自分が知っていることでも再びクロスチェックすることが重要だと思いました。重要なロジックは全て実装しましたが、このようなところでエラーがあるとは思いもしませんでした… :(
  • リスト変数はオブジェクトなのでプリミティブタイプ変数とは異なりアドレス値を持っている点。ポスティングをご覧になっている皆さんもよく覚えていて、私と同じミスをしないようにしてくださいね!