【Python】関数を呼ぶたびにリストが増える?可変デフォルト引数を直す

PythonのTopに戻る


関数を呼ぶたびに前回の要素まで増えていく場合は、引数の既定値に[]や{}を置いていないか確認する。デフォルト引数は呼び出すたびではなく、関数を定義するときに評価される。呼び出しごとに新しいリストを作りたいなら、既定値をNoneにし、関数内で初期化する。

既定のリストは呼び出し間で共有される

結果の記録用にitems=[]と書くと、引数を省略するたびに同じリストが使われる。そのリストへappendすると、変更した内容が次回まで残る。関数の中で作ったつもりでも、既定値として置いた場所が関数定義の一部であることがポイントになる。

次のbad_recordは、その挙動を確かめるための失敗例である。最初の戻り値firstも、二回目の呼び出し後には要素が増えている。戻り値を別の変数へ保存したことだけでは、リストの複製にはならない。

Noneを使って呼び出しごとに初期化する

example.py

def bad_record(value, items=[]):
    items.append(value)
    return items

first = bad_record("A")
second = bad_record("B")
print("shared default:", first, second, first is second)
assert first == ["A", "B"] and first is second


def record(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items


a = record("A")
b = record("B")
print("fresh defaults:", a, b, a is b)
assert a == ["A"] and b == ["B"] and a is not b

shared = []
result = record("C", shared)
print("explicit list:", shared, result is shared)
assert shared == ["C"] and result is shared

実行結果

shared default: ['A', 'B'] ['A', 'B'] True
fresh defaults: ['A'] ['B'] False
explicit list: ['C'] True

修正後はitemsがNoneのときだけ、新しいリストを作る。aとbの値は別々に保たれ、isによる確認もFalseになる。最後のsharedは呼び出し側から明示的に渡したリストなので、そこへの追加は呼び出し元にも反映される。

空リストと引数の省略を混同しない

if not itemsと書くと、Noneだけでなく空リストも初期化の対象になる。呼び出し元から渡された空リストを更新したい設計では、別のリストへ置き換わってしまう。今回の目的ならif items is Noneと判定する方が意味が明確である。

一方、渡されたリストを変更しない関数にしたいなら、None以外の場合もlist(items)などで新しい入れ物を作る。ただし、入れ子のオブジェクトまでは独立しない。関数が入力を更新するか、新しい結果を返すかを先に決め、その契約に合うコピーを選ぼう。

辞書や集合にも同じ問題が起こる

この問題はリストに限らない。設定をためる辞書、既読のIDを記録する集合、独自クラスのインスタンスなど、後から状態を変更できるオブジェクトを既定値へ置くと、共有の影響を受ける。小さな空の入れ物であっても、可変である点は同じである。

数値や文字列のような不変オブジェクトでは、appendのように同じ中身を変更する操作はできない。そのため同じ形の問題は起こりにくい。ただし、タプル自体が不変でも、その中にリストを含めれば内部のリストは変更できる。外側の型だけで安全と判断しないようにしたい。

評価される時点にも注意する

既定値が決まるのは、def文が実行されたときである。例えば時刻を取得する関数を既定値に直接書くと、呼び出し時刻ではなく定義時刻が使われる。毎回新しい値が必要なら、関数の本体で取得するという同じ考え方が使える。

クロージャーの生成時に値を固定したいなど、定義時の評価を意図して利用する場面もある。既定値を使うこと自体が悪いのではなく、状態を共有したいのか、呼び出しごとに独立させたいのかを明示することが重要である。

二回続けて呼んで確かめる

動作確認を一回の呼び出しだけで済ませると、この不具合は見つけにくい。引数を省略して二回呼び、最初の戻り値が後から変わらないかを確認する。さらに、明示的に空リストを渡した場合の動作も、意図どおりかを見ておくとよい。

None自体を有効な入力値として使いたい場合は、未指定との区別が必要になる。そのときは専用のセンチネルを使う設計へ進む。まずは、引数省略時の新規作成と、明示的に渡されたオブジェクトの扱いを分けることから始めよう。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る