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)はこのパターンを警告してくれる。