🔖

【Python】デフォルト引数にリストや辞書を使うと意図しない動作が起きる理由とは?

に公開1

はじめに

Pythonで関数を定義する際、次のようなコードを見かけることがあります。

def append_item(item, lst=[]):
    lst.append(item)
    return lst

一見すると、「lstitem を追加して返すシンプルな関数」に思えます。

しかし、この関数を複数回呼び出すと、前回追加した値が残ってしまう予期しない動作になります

本記事では、その原因である
「ミュータブルなデフォルト引数(mutable default argument)」に関する重要な注意点について解説します。

結論:ミュータブルなデフォルト引数は避けるべき

リストや辞書といった変更可能(ミュータブル)なオブジェクトをデフォルト引数に指定すると、関数の外に影響が残ってしまうことがあります。

このため、Pythonでは None をデフォルト値にし、関数内で初期化するのが推奨されます。

具体例:どのような問題が起きるのか

以下の関数をご覧ください。

def append_item(item, lst=[]):
    lst.append(item)
    return lst

この関数を2回呼び出すと、次のような結果になります。

print(append_item('apple'))   # ['apple']
print(append_item('banana'))  # ['apple', 'banana'] ← 予想外の結果

2回目の呼び出しで、1回目の内容が残ってしまっています。

なぜこのような挙動になるのか?

Pythonでは、デフォルト引数が関数定義時に一度だけ評価されるという仕様があります。

つまり lst=[] の部分は関数が定義されたときに1回だけ実行され、その後はすべての関数呼び出しで同じリストオブジェクトが再利用されます。

そのため、リストに要素を追加すると、その変更が次回の関数呼び出しまで残ってしまうのです。

def append_item(item, lst=[])

上記関数の lst は関数定義時に1回だけ生成され、その後も同じリストがすべての呼び出しで使われます。

正しい書き方:None を使って初期化する

この問題を避けるためには、以下のように None を使ってデフォルト値を設定し、関数内でリストを初期化します。

def append_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

このように書くことで、毎回新しいリストが生成され、呼び出しごとに状態がリセットされます。

print(append_item('apple'))   # ['apple']
print(append_item('banana'))  # ['banana']

応用:辞書やセットにも同じ注意が必要

この問題はリストだけでなく、辞書やセットなどの他のミュータブルなオブジェクトでも同様に発生します

def add_key_value(key, value, dct={}):  # 推奨されない書き方
    dct[key] = value
    return dct

この場合も、呼び出しのたびに同じ辞書が使われるため、意図しない結果になります。

正しい書き方は以下のとおりです。

def add_key_value(key, value, dct=None):
    if dct is None:
        dct = {}
    dct[key] = value
    return dct

まとめ

書き方 挙動 コメント
def func(x=[]) ❌ 意図しないバグの原因になる リストや辞書が共有される
def func(x=None) + 関数内で初期化 ✅ 安全な書き方 呼び出しごとに新しく作られる

おわりに

Pythonはシンプルに書ける一方で、言語仕様を正しく理解していないと、予期せぬ挙動に繋がることがあるため注意が必要です。

本記事で取り上げた「ミュータブルなデフォルト引数に関する仕様」も、つまずきやすいポイントの一つです。

本記事が理解の一助となれば幸いです。

Discussion