Python でも変数の参照などは理解しないといけないね、っておはなし

# 背景

同僚が、Python でなんかバグに遭遇していた。

- 波形データの合成処理で、髭が付加する

という現象で、再現性もあるってことだったので、デバッグしたらすぐじゃない？と傍観してたが、意外に苦戦してる様子だったので、コードを一式貰って解析してみた際の記録

# 結論

ミュータブル(mutable)、イミュータブル(immutable) の変数をよく理解せず使ってしまっていた。

具体的には・・
- list の内容を保存していこうとしているにも関わらず、listを渡すだけで保存したつもりになっていた。

通常であれば問題になることは少ないが、今回問題になったのは、

- 引数でlistを共有して、処理側で変更して戻していた為

であった。

引数自体を毎回変えているにもかかわらず、それを利用する側でその認識が落ちていた為、初回保存データが消えており、二回目データが二重に記録されることで、波形に髭が出たように見えていた。
   ※最初のデータが記録されず、二回目のデータが二重に記録されていた 

::: note

- mutable, immutable という単語について

`mute` 音が出ている状態を定常とし、静かにする＝変える、と捉えると分かりやすい。

im-: 否定の接頭語
mute: 音を消す、静かにする。変える(mutare)。語源はラテン語 mutus
able: 可能を示す接尾語

- mutable: 変更可能な
- immutable: 変更不可能な

:::

# 詳細

## 具体的な 再現 sample code 

```python: mutable list

# グローバル変数の初期化
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=}")  # 影響を確認


```

## 実行結果

```powershell: result


---- 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]

```



## 解説

### 問題点

以下のように、初回データが消えている

```powershell
---- 参照を使用する場合 ----
record_data_ref: record_data_ref=[1, 2, 3]
record_data_ref: record_data_ref=[4, 5, 6, 4, 5, 6]
```

以下で、outdata 自体を置き換えている

```python
outdata[:] = [4, 5, 6]
```

record_data_ref を以下で保存したように処理しているが、
単に outdata の参照を保持しているだけである為、二回目の呼び出しによって同一データを concatenate() することになっているのが原因であった。 

```python 
# 参照を使用する関数
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)
```

ということで、以下のような対処策がある。個人的には前者かな 

1. 保存する際に、データをそのまま渡す（共有する）のではなく、データコピーを行う
1. 引数に戻す処理と、そこから利用する変数を別にする

# あとがき

python でやってると、あまり参照を意識することが無いからこそ、のバグだったのかなと

こういう点では組込出身者の方が、メモリを意識しているところに一日の長がある、筈 :thinking:

まぁ、結局はその人の性格と、姿勢・・かなぁとも :sweat_smile: