今日は、Pythonの[[]]*3に潜む罠の日。
目次
とある日
Deep Learningのために、会話データを学習データのために整形しようとしたときに起こった悲劇。
temp = ["a","b","c"] second = dict(zip(temp,[[]]*len(temp))) print(second) second["a"].append("a") print(second)
{'a': [], 'b': [], 'c': []} {'a': ['a'], 'b': ['a'], 'c': ['a']}
second["a"]のvalueに値を、代入したいと思っていたのに全てのvalueに値が代入される。
なぜ?
この謎の解明と解決をやっていきます。
作成しようとしたプログラムの内容
Deep Learningの技術を使用して、対話ボットを作成しようしていた。
そのため、学習に必要な対話データをネットから拾った。
下記のような形式でデータがまとまっている。
@ミホーク:何を・・・ @ゾロ:背中の傷は剣士の恥だ @ミホーク:見事
これを、@ユーザごとに会話したデータを保持したいと考えた。
data{ "ミホーク":["何を・・・","見事"], "ゾロ":["背中の傷は剣士の恥だ"] }
こんな感じにしたい。
ただ、対話データも会話が交互にずっとなっているわけではない。
@ゾロ:不安にさせたかよ…おれが…… 世界一の…剣豪にくらいならねェと…お前が困るんだよな………!!! おれはもう!! 二度と敗けねェから!!!! あいつに勝って大剣豪になる日まで 絶対に もう おれは敗けねェ!!!! 文句あるか 海賊王!! @ルフィ:しししし!! ない!!!
このように、一人が複数行に渡って話していることもある。
なので、今誰が話しているかの状態を保持する変数を使って、対話データの選別を行おうと考えた。
ということで、ユーザをkeyにして話した内容をvalueとするdictを作成しようとした。
{'a': [], 'b': [], 'c': []}
悲劇の始まり
keyに使うユーザ名は、別で対話データを、受け取るとユーザリストを作成し返却する関数としてとして実装した。
@ミホーク:何を・・・ @ゾロ:背中の傷は剣士の恥だ @ミホーク:見事
の対話データを渡すと。
["@ミホーク","@ゾロ"]
こんな感じで返ってくる。
これをkeyとして対話データ格納用listを新しい変数として作成したい。
key = ["@ミホーク","@ゾロ"] user_list = dict(zip(key,[[]]*len(key)))
こんな感じで作成完了。
少し解説。
dict() → listをdictに変換するため
zip() → 2つのlistを結合して一つのlistを生成
len() → 引数の要素数を返す
[[]]*n → 配列を複製する方法nに任意の数を指定して指定した要素の配列を生成
これら合わせて、文字で説明すると、keyとlenでkeyの数を調べその数だけ空の配列を生成してそれらをzipで結合した後、dictで辞書配列に変換する。ということになる。
そしてこの配列に、対話データを追加すればいい。
と思ったら、ミホークとゾロに同じ内容が追加されている!?
data = { "@ミホーク":["何を・・・","背中の傷は剣士の恥だ","見事"], "@ゾロ":["何を・・・","背中の傷は剣士の恥だ","見事"] }
なぜ。
原因
対話データを格納する変数のdictを生成するところが問題だった。
user_list = dict(zip(key,[[]]*len(key)))
このlist生成部分が悪さをしていた。
[[]]*len(key)
この複製方法だと、同じインスタンスを参照していて、一つの箇所を変更するとそれに付随して全てが変更されることになっていた。
原因を見つけるためにやったこと
追加方法変更
対話データを代入する部分が原因かと目をつけた。
user_listに対話データを代入するときに、appendで追加していたのだが、それを添字指定をして代入することにした。
すると、Python list index out of rangeになる。
あれ、list型が入ってるよなと思い確認したら、問題なかった。
追加方法でも謎があったが、これは関係なかった。
自動ではなく手動生成
そもそも、意図した動きが可能かどうかを検証した。
first = {"a":[],"b":[],"c":[]}
理想形の変数を作成
first["a"].append("a")
データ代入
first = {"a":["a"],"b":[],"c":[]}
あれ、イケてる。
なぜ。
でもこれで、変数の生成箇所が原因とわかった。
解決方法
一つ一つ別のインスタンスを生成する必要がある。
None_List = [] member_list = ["@ミホーク","@ゾロ"] for index in member_list: None_List.append(copy.deepcopy([])) result_list = dict(zip(member_list,None_List))
Python3には、インスタンスの複製としてcopy1という関数が存在している。
copy.``copy
(x)x の浅い (shallow) コピーを返します。
copy.``deepcopy
(x[, memo])x の深い (deep) コピーを返します。
copy.copyだと自分がやったことと同じインスタンスが同じとなるので、したいことは違う。
一方、deepcopyだとそうはならないようだ。
〆
Python でプログラムを書いていて、あまり気にしなかったインスタンスのことや、普段やらない書き方をしたこともあり自分ではなぞの解明ができなかった。
有識者の意見を借りて謎を解明した。
自分でも気付けるようにしたいし、こういった罠に引っかからないようにしたい。
まだまだ、わからないことが多いと思う今日このごろ。