Python で最も有名なアンチパターンの一つが、ミュータブルなデフォルト引数だ。特にクラスの __init__ で発生しやすく、見つけにくいバグの原因になる。
問題のあるコード
class ShoppingCart: def __init__(self, items=[]): self.items = items def add(self, item): self.items.append(item) # 使ってみる cart1 = ShoppingCart() cart1.add("りんご") print(cart1.items) # ['りんご'] cart2 = ShoppingCart() print(cart2.items) # ['りんご'] ← なぜ?
cart2 は新しいインスタンスなのに、cart1 で追加した「りんご」が入っている。これは意図した動作ではないはずだ。
なぜこうなるのか
デフォルト引数は関数定義時に一度だけ評価される。つまり items=[] の空リストは、クラス定義時に作られた 1 つのリストオブジェクトを指し続ける。
期待する動作
インスタンスごとに新しい空リストが作られる
実際の動作
すべてのインスタンスが同じリストオブジェクトを共有する
class ShoppingCart: def __init__(self, items=[]): self.items = items cart1 = ShoppingCart() cart2 = ShoppingCart() print(id(cart1.items)) # 140234567890 print(id(cart2.items)) # 140234567890 ← 同じ id print(cart1.items is cart2.items) # True
正しい書き方
ミュータブルなデフォルト引数には None を使い、関数内で初期化する。
class ShoppingCart: def __init__(self, items=None): if items is None: items = [] self.items = items def add(self, item): self.items.append(item) cart1 = ShoppingCart() cart1.add("りんご") cart2 = ShoppingCart() print(cart2.items) # [] ← 期待どおり空
あるいは、より簡潔に書くこともできる。
class ShoppingCart: def __init__(self, items=None): self.items = items if items is not None else []
リストだけではない
この問題はリストに限らない。辞書、セット、その他のミュータブルなオブジェクトすべてで起きる。
# すべて危険 def bad_func(data={}): ... def bad_func(items=set()): ... def bad_func(config=SomeMutableClass()): ... # 安全 def good_func(data=None): if data is None: data = {}
なぜ Python はこういう仕様なのか
一見バグに見えるこの仕様だが、理由がある。デフォルト引数を呼び出しごとに評価すると、関数呼び出しのたびにオブジェクト生成コストがかかる。また、デフォルト引数をキャッシュとして利用するテクニックもある。
# メモ化のテクニック(意図的な利用例) def fibonacci(n, cache={0: 0, 1: 1}): if n not in cache: cache[n] = fibonacci(n - 1) + fibonacci(n - 2) return cache[n]
ただし、このテクニックは可読性が低いため、functools.lru_cache を使うほうが望ましい。
覚えておくべきルール
デフォルト引数にはイミュータブルな値を使う
None、数値、文字列、タプルなどは安全。リスト、辞書、セットは危険。
ミュータブルな初期値が必要なら None を使う
関数内で if arg is None: のパターンで初期化する。これが Python の慣習。
この罠は Python を何年書いていても踏むことがある。IDE や Linter(pylint、flake8)はこのパターンを警告してくれる。