プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2022/08/13

[Python] アドレス (id) は変更せずに辞書 (dict) をコピーしたい

event_note2022/08/13 2:08

わかってしまえば当たり前なのですが、ちょっとハマったのでメモ。

結論から先に言うと、clear() update() を使うことで実現できました(ただし排他には要注意)。

環境

  • Python 3.8.10

前提知識

以下の話はググれば解説されているページがいくらでも見つかりますが・・・。

dict はミュータブルな型なので、代入する場合には注意が必要です。

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

dict_a = dict_b # 代入

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

代入した場合には同じオブジェクトを指すようになるので、以下のように id が同じになります。

dict_a={'key_a': 'value_a'}, id:140351608696704
dict_b={'key_b': 'value_b'}, id:140351608696768
----------
dict_a={'key_b': 'value_b'}, id:140351608696768
dict_b={'key_b': 'value_b'}, id:140351608696768

なので、dic_a を変更したら dict_b の中身も変わります。

これを回避するには(つまり id は別で中身だけをコピーしたい場合は) copy() を使います。

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

dict_a = dict_b.copy()

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

この場合は新たにオブジェクトが作成されるので、id は別で中身は同じになります。

dict_a={'key_a': 'value_a'}, id:139764478058368
dict_b={'key_b': 'value_b'}, id:139764478058432
----------
dict_a={'key_b': 'value_b'}, id:139764476783232
dict_b={'key_b': 'value_b'}, id:139764478058432

じゃあ id を変えずに中身だけをコピーするには?

clear()update() を使えばできます。

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

dict_a.clear()
dict_a.update(dict_b)

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')
dict_a={'key_a': 'value_a'}, id:139790269009792
dict_b={'key_b': 'value_b'}, id:139790269009856
----------
dict_a={'key_b': 'value_b'}, id:139790269009792
dict_b={'key_b': 'value_b'}, id:139790269009856

dict_a dict_b の中身は同じですが、dict_aid は変わっていません。

関数に引数で渡す場合

Python では全て参照渡しなので、dict を関数に渡して関数内で変更した場合、その関数の外にも影響を与えます。


def change_dict(target_dict):
    target_dict['hoge'] = 'piyo'

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

change_dict(dict_a)

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')
dict_a={'key_a': 'value_a'}, id:139844338024320
dict_b={'key_b': 'value_b'}, id:139844338024384
----------
dict_a={'key_a': 'value_a', 'hoge': 'piyo'}, id:139844338024320
dict_b={'key_b': 'value_b'}, id:139844338024384

本題

じゃあ、関数内で dict を丸ごと差し替えたい場合はどうすればよいか?
引数で渡した値に丸ごと新しい dict を代入すると、その引数の参照先が変わるだけなので、参照元には反映されません。

def change_dict(target_dict):
    target_dict = {'hoge': 'piyo'}

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

change_dict(dict_a)

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')
dict_a={'key_a': 'value_a'}, id:139712684732288
dict_b={'key_b': 'value_b'}, id:139712684732352
----------
dict_a={'key_a': 'value_a'}, id:139712684732288
dict_b={'key_b': 'value_b'}, id:139712684732352

かと言って、copy() を使うと新たにオブジェクトが作成されるので、この場合も参照元には反映されません(コピー前のオブジェクトを参照しているため)。

def change_dict(target_dict):
    target_dict = {'hoge': 'piyo'}.copy()

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

change_dict(dict_a)

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')
dict_a={'key_a': 'value_a'}, id:2642357453888
dict_b={'key_b': 'value_b'}, id:2642357181760
----------
dict_a={'key_a': 'value_a'}, id:2642357453888
dict_b={'key_b': 'value_b'}, id:2642357181760

なので、参照元にも変更を反映したい場合には、前述したように clear() update() を使います。

def change_dict(target_dict):
    target_dict.clear()
    target_dict.update({'hoge': 'piyo'})

dict_a = {"key_a": "value_a"}
dict_b = {"key_b": "value_b"}

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')

print(f'----------')

change_dict(dict_a)

# 値と id の確認
print(f'dict_a={dict_a}, id:{id(dict_a)}')
print(f'dict_b={dict_b}, id:{id(dict_b)}')
dict_a={'key_a': 'value_a'}, id:2371211616000
dict_b={'key_b': 'value_b'}, id:2371211612864
----------
dict_a={'hoge': 'piyo'}, id:2371211616000
dict_b={'key_b': 'value_b'}, id:2371211612864

排他には注意

clear() update() という2つの関数を使って dict を更新するので、並列処理を行っている場合には排他が必要です。
clear()update() の間で値が読み込まれるということが発生しうるからです。

私の場合、マルチプロセスで動いているアプリで実際にそれが発生しました。
(使っていたのは普通の dict ではなく multiprocessing.Manager().dict() ですが)

参考 URL