様々な事情によりデプロイした後の火消しが発生する。
トラブルシュートの結果、リストに想定外の値が格納されていることが判明したとしよう。さて何が起こったのだろう。下記に限らず様々な理由があるだろう。
値の初期化を適切な箇所で行っていなかった
異なる変数が意図せず同じアドレスを参照していた
機能追加など何らかの理由で引数に渡すリストの要素数が増えていた
実際のプログラムでは、メインからモジュールで定義された関数を呼び出し、関数内でクラスをインスタンス化し、ループ処理でインスタンス変数へ値がappendされている…というように何が発生しているのか突き止めるためにブレークポイントを指定して処理を追いかけてゆくことになると思う。些細な誤りだとしても仮説を丹念に検証してようやく答えにたどり着くものだ。
後工程になればなるほど解析し辛い環境になりがちのため、可能な限りテスト工程の初期段階で不具合の洗い出しと改修はある程度終えていたい。
call by address(複数の変数が同じアドレスを参照した状態)の例:
>>> words
['foo', 'bar', 'baz', 'waldo']
>>> words2 = words
>>> words2[0] = 'boo' # 先頭の要素に再代入する
>>> words
['boo', 'bar', 'baz', 'waldo'] # words2の先頭の要素へのみ再代入したつもりだったが・・・
>>> words2
['boo', 'bar', 'baz', 'waldo']
>>> id(words)
4473990912
>>> id(words2)
4473990912 # IDが同じだった(同じアドレスを参照していた)
値のコピー(ある変数の値をコピーして異なる変数へ代入した状態)の例:
>>> words3 = words[:] # wordsの値がコピーされる
>>> id(words3)
4474968576 # 異なるIDになった(異なるアドレスを参照していた)
>>> id(words)
4473990912
>>> words3[0] = 'hoo'
>>> words
['boo', 'bar', 'baz', 'waldo'] # 先頭の要素に変化なし
>>> words2
['boo', 'bar', 'baz', 'waldo'] # 先頭の要素に変化なし
>>> words3
['hoo', 'bar', 'baz', 'waldo'] # このリストだけが変更される
>>> words4 = words.copy() # コピーメソッドでコピーする
>>> id(words4)
4473996736 # 異なるIDになった(異なるアドレスを参照していた)
補足すると複数の変数が同じアドレスを参照することは悪ではない。異なるリストとして使い分けたい際に同じアドレスを参照するのはアプローチとして誤っているのだ。
下記のような操作を行う場合、コピーメソッドでは求める結果を得られないかも知れない。
>>> words = [['foo', 'bar', 'baz']]
>>> new_words = ['qux', 'quux', 'corge', ['grault', 'garply']]
>>> words + [new_words]
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['grault', 'garply']]]
>>> all_words = words + [new_words]
>>> all_words2 = all_words.copy()
>>> id(all_words)
4474968128 # 異なるIDになった
>>> id(all_words2)
4474988736 # 異なるIDになった
>>> all_words
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['grault', 'garply']]]
>>> all_words2
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['grault', 'garply']]]
>>> all_words2[1][3][0] = 'fred' # all_words2の要素へ再代入すると・・・
>>> all_words
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['fred', 'garply']]] # all_wordsも変わってしまった
>>> all_words2
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['fred', 'garply']]]
多次元リストをコピーするならばdeepcopyが適切だろう。
>>> from copy import deepcopy
>>> all_words3 = deepcopy(all_words)
>>> words = [['foo', 'bar', 'baz']]
>>> new_words = ['qux', 'quux', 'corge', ['grault', 'garply']]
>>> all_words = words + [new_words]
>>> from copy import deepcopy
>>> all_words3 = deepcopy(all_words)
>>> all_words3[1][3][0] = 'fred' # all_words3の要素へ再代入すると・・・
>>> all_words
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['grault', 'garply']]]
>>> all_words3
[['foo', 'bar', 'baz'], ['qux', 'quux', 'corge', ['fred', 'garply']]]
>>> id(all_words)
4475133504
>>> id(all_words3)
4474968128
>>> all_words2 = all_words.copy()
>>> id(all_words2)
4474988672
>>> id(all_words[1][3])
4474988928
>>> id(all_words2[1][3])
4474988928 # all_words2[1][3]とall_words[1][3]は同じIDだった
>>> id(all_words3[1][3])
4475133824 # all_words3[1][3]は異なるIDになった
さて、そもそも代入とは何を行っているのだろう。
まとめ:
変数へ代入する際は値をコピーしているのか、アドレスを参照しているのか意識しよう
多次元リストのコピーはdeepcopyを使おう
レビュを実施しよう
レビュアーに有識者をアサインしよう
要員の負担になり難いセルフチェックの精度を向上させる仕組み作りをしよう
変更が発生したら必ずテストせざる得ない仕組み作りをしよう
テスト結果は確認/評価しよう