題名の通りです。Python ではデフォルト引数に list や dict などの mutable object を直接セットすると、予期しない挙動を引き起こしやすいです。
具体的に何が起こるかというと、次のようになります。
def add(name: str, name_list: list[str] = []) -> list[str]: name_list.append(name) return name_list if __name__ == '__main__': print(add('太郎')) # ['太郎'] print(add('次郎')) # ['太郎', '次郎']
引数の name_list が使いまわされ、一度呼び出した時の加工結果が二回目にも使われています。これは次の様なコードを書く時に問題になります。
def append_name_to_list(*name: str, base_name_list: list[str] = []) -> list[str]: """ 渡された名前をリストに追加する関数 :param name: 追加する名前(可変長引数) :param base_name_list: 名前を格納するリスト :return: 渡された名前が追加されたリスト """ base_name_list.extend(name) return base_name_list if __name__ == '__main__': print(append_name_to_list('太郎', '次郎')) # ['太郎', '次郎'] print(append_name_to_list('佐藤')) # ['太郎', '次郎', '佐藤']
このコードは文字列をリストに追加する関数にデフォルトの挙動としてリストを自動生成する機能をつけたコードです。期待する挙動は 「base_name_list が None の場合、渡された名前のリストを生成する」というものですが、実際はこれまで渡された名前たち全てのリストが返されています。これに限らず引数に list や dict があってもなくてもよくて、ないならとりあえず初期状態の list や dict を用意する、という関数はこの書き方をすると不具合を起こしやすいです。ちなみに不具合を起こさない場合は、問題となるデフォルト引数を使うタイミングがプログラムの全フローの中で一度以下の場合です。この場合、二回目の初期化を試みようとすることがないのでバグが顕在化しません。
対処方法は次です。
def append_name_to_list(*name: str, base_name_list: list[str] = None) -> list[str]: """ 渡された名前をリストに追加する関数 :param name: 追加する名前(可変長引数) :param base_name_list: 名前を格納するリスト。デフォルト値としてNoneが指定されています。 この引数に値を指定しなかった場合、内部で空のリストが生成されます :return: 渡された名前が追加されたリスト """ # デフォルト引数は空 list ではなく None にする # もし None だった場合は関数の中で新たに空 list を生成することで使いまわしを防ぐ if base_name_list is None: base_name_list = [] else: base_name_list = base_name_list[:] # 元 list を壊さないための list の複製 base_name_list.extend(name) return base_name_list if __name__ == '__main__': print(append_name_to_list('太郎', '次郎')) # ['太郎', '次郎'] print(append_name_to_list('佐藤')) # ['佐藤']
デフォルト引数に mutable object である list の値を直に指定するのをやめ、代わりに None を置きます。もし None のまま関数が実行されたらデフォルト引数相当の空 list を引数に代入します。こうすることで予期せぬオブジェクトの使いまわしを防げます。
割と手間ですが、そもそも mutable オブジェクトを使わないという方法もあります。例えば次の様に全てを tuple にすることができます。
def append_name_to_tuple(*name: str, base_name_tuple: tuple[str, ...] = ()) -> tuple[str, ...]: """ 渡された名前をタプルに追加する関数 :param name: 追加する名前(可変長引数) :param base_name_tuple: 名前を格納するタプル。デフォルト値として空のタプルが指定されています。 :return: 渡された名前が追加された tuple """ return base_name_tuple + name if __name__ == '__main__': print(append_name_to_tuple('太郎', '次郎')) # ['太郎', '次郎'] print(append_name_to_tuple('佐藤')) # ['佐藤']
list や dict の様な mutable object を扱わないのであれば if や None を書くべきか書かないべきか悩む必要がないわけです。もっとも大分不便になるので微妙なやり方でもありますが。
私的におすすめなのは IDE の支援を使うことです。問題があれば検知し、手間な書き換えも自動でやってくれます。例えば PyCharm ならば次の様に動いてくれます。