背景
同僚が、Python でなんかバグに遭遇していた。
- 波形データの合成処理で、髭が付加する
という現象で、再現性もあるってことだったので、デバッグしたらすぐじゃない?と傍観してたが、意外に苦戦してる様子だったので、コードを一式貰って解析してみた際の記録
結論
ミュータブル(mutable)、イミュータブル(immutable) の変数をよく理解せず使ってしまっていた。
具体的には・・
- list の値を保存していこうとしているにも関わらず、参照渡しで保存したつもりになっていた。
通常であれば問題になることは少ないが、今回問題になったのは、
- 引数を 参照渡しとして、処理側で変更して戻していた為
であった。
引数自体を毎回変えているにもかかわらず、それを利用する側でその認識が落ちていた為、初回保存データが消えており、二回目データが二重に記録されることで、波形に髭が出たように見えていた。
※最初のデータが記録されず、二回目のデータが二重に記録されていた
- mutable, immutable という単語について
mute
音が出ている状態を定常とし、静かにする=変える、と捉えると分かりやすい。
im-: 否定の接頭語
mute: 音を消す、静かにする。変える(mutare)。語源はラテン語 mutus
able: 可能を示す接尾語
- mutable: 変更可能な
- immutable: 変更不可能な
詳細
具体的な 再現 sample code
# グローバル変数の初期化
record_data_copy = [] # copyを使用
record_data_ref = [] # 参照を使用
# コピーを使用する関数
def record_with_copy(outdata):
global record_data_copy
copied_data = outdata.copy()
if len(record_data_copy) == 0:
record_data_copy = copied_data
else:
record_data_copy.extend(copied_data)
# 参照を使用する関数
def record_with_reference(outdata):
global record_data_ref
if len(record_data_ref) == 0:
record_data_ref = outdata
else:
record_data_ref.extend(outdata)
def perform_modifications(outdata):
global record_data_ref
# コピーを使用する場合のテスト
print("---- Copyを使用する場合 ----")
outdata = []
outdata[:] = [1, 2, 3]
record_with_copy(outdata) # 最初のデータ
print(f"record_data_copy:{record_data_copy=}") # 結果を表示
outdata[:] = [4, 5, 6]
record_with_copy(outdata) # 次のデータ
print(f"record_data_copy: {record_data_copy=}") # 結果を表示
# 参照を使用する場合のテスト
print("\n---- 参照を使用する場合 ----")
outdata = []
outdata[:] = [1, 2, 3]
record_with_reference(outdata) # 最初のデータ
print(f"record_data_ref: {record_data_ref=}") # 結果を表示
outdata[:] = [4, 5, 6]
record_with_reference(outdata) # 次のデータ
print(f"record_data_ref: {record_data_ref=}") # 結果を表示
# 参照を使用する場合のテスト
print("\n---- よくある参照でも問題ない場合 ----")
outdata = []
record_data_ref = []
outdata = [1, 2, 3]
record_with_reference(outdata) # 最初のデータ
print(f"record_data_ref: {record_data_ref=}") # 結果を表示
outdata = [4, 5, 6]
record_with_reference(outdata) # 次のデータ
print(f"record_data_ref: {record_data_ref=}") # 結果を表示l
outdata = []
perform_modifications(outdata)
print("\nAfter modifying outdata1:")
print(f"record_data_copy(after modification): {record_data_copy=}") # 影響を確認
print(f"record_data_ref (after modification): {record_data_ref=}") # 影響を確認
実行結果
---- Copyを使用する場合 ----
record_data_copy:record_data_copy=[1, 2, 3]
record_data_copy: record_data_copy=[1, 2, 3, 4, 5, 6]
---- 参照を使用する場合 ----
record_data_ref: record_data_ref=[1, 2, 3]
record_data_ref: record_data_ref=[4, 5, 6, 4, 5, 6]
---- よくある参照でも問題ない場合 ----
record_data_ref: record_data_ref=[1, 2, 3]
record_data_ref: record_data_ref=[1, 2, 3, 4, 5, 6]
After modifying outdata1:
record_data_copy(after modification): record_data_copy=[1, 2, 3, 4, 5, 6]
record_data_ref (after modification): record_data_ref=[1, 2, 3, 4, 5, 6]
解説
問題点
以下のように、初回データが消えている
---- 参照を使用する場合 ----
record_data_ref: record_data_ref=[1, 2, 3]
record_data_ref: record_data_ref=[4, 5, 6, 4, 5, 6]
以下で、outdata 自体を置き換えている
outdata[:] = [4, 5, 6]
record_data_ref を以下で保存したように処理しているが、
単に outdata の参照を保持しているだけである為、二回目の呼び出しによって同一データを concatenate() することになっているのが原因であった。
# 参照を使用する関数
def record_with_reference(outdata):
global record_data_ref
if len(record_data_ref) == 0:
record_data_ref = outdata
else:
record_data_ref.extend(outdata)
ということで、以下のような対処策がある。個人的には前者かな
- 保存する際に、参照渡しではなく、データコピーを行う
- 引数に戻す処理と、そこから利用する変数を別にする
あとがき
python でやってると、あまり参照渡しとか意識することが無いからこそ、のバグだったのかなと
こういう点では組込出身者の方が、メモリを意識しているところに一日の長がある、筈
まぁ、結局はその人の性格と、姿勢・・かなぁとも