NAKED WETWARE

「わがままになるのが怖い奴に宇宙は拓けねェさ」

Pythonで二次元配列の初期化では「参照渡し」に気をつけないといけない

問題

先日、プログラミングコンテストで、バグチックな変な問題に足元をすくわれました。

それは、初期化した二次元配列の一部の値を更新したら、ほかの値も更新されるというものです。

以下のようなコードです。

# /usr/bin/python3
# coding: utf8

INF = 100010001001

if __name__ == '__main__':
    H, W = 4, 4
    # 二次元配列を準備
    memo = [[INF]*W]*H

    # (0,0)を0で更新
    memo[0][0] = 0

    print(*memo, sep="\n")
    # n行0列がすべて0で更新されてしまっている
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]

原因

どうも、参照渡し系に起因する問題みたいでした。

for文と使って丁寧に初期化すると問題は起きず、ワンライナーで書くと問題が発現します。

    # 手寧な初期化
    memo = []
    for _ in range(H):
        memo.append([INF] * W)
    memo[0][0] = 0
    print(*memo, sep="\n")
    # (0,0)だけが0になってる
    # [0, 100010001001, 100010001001, 100010001001]
    # [100010001001, 100010001001, 100010001001, 100010001001]
    # [100010001001, 100010001001, 100010001001, 100010001001]
    # [100010001001, 100010001001, 100010001001, 100010001001]

    # 繰り返し演算字を使ったワンライナー
    memo_oneliner = [[INF]*W]*H
    memo_oneliner[0][0] = 0
    print(*memo_oneliner, sep="\n")
    # 0列すべて0
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]

参照値(識別値)についてみてみます。

    # 手寧な初期化
    memo = []
    for _ in range(H):
        memo.append([INF] * W)
    for row in memo:
        print(id(row))
    # 識別値は全行で異なる
    # 140091744609096
    # 140091744586440
    # 140091744917000
    # 140091744586952

    # 繰り返し演算字を使ったワンライナー
    memo_oneliner = [[INF]*W]*H
    for row in memo_oneliner:
        print(id(row))
    # 識別値が全行で一致
    # 140091744586504
    # 140091744586504
    # 140091744586504
    # 140091744586504

繰り返し演算子を使うワンライナーのほうでは、全行で識別値が一致しています。

どうもPythonでは、繰り返し演算子を使う場合、新しいオブジェクト(この場合はlistオブジェクト)が都度作成されるわけでなく、 一つ作ったオブジェクトを使いまわすみたいですね。

イメージとしては

    # memo = [[INF]*W]*H のイメージ
    row = [INF]*W
    memo = []
    for _ in range(H):
        memo.append(row)

    # 検証
    memo[0][0] = 0
    print(*memo, sep="\n")
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    # [0, 100010001001, 100010001001, 100010001001]
    for row in memo:
        print(id(row))
    # 140503221013320
    # 140503221013320
    # 140503221013320
    # 140503221013320

という感じです。

解決策

多次元配列を一意の値で初期化する場合、丁寧に初期化するのがよさそうです。

そのための、多次元配列初期化関数でも用意しておくのもいいかもしれません。

これでQOLがちょっぴり上がった。