(重要)Python のデフォルト引数は関数定義時に一度だけ評価される

Python の関数でデフォルト引数を指定すると、その値は関数が定義されたときに一度だけ評価され、以降の呼び出しでは同じオブジェクトが使い回される。この挙動は他の多くの言語と異なるため、予期しないバグの原因になりやすい。

def append_to(element, target=[]):
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] ← 期待は [2] だが...
print(append_to(3))  # [1, 2, 3]

上のコードでは target=[] というデフォルト引数が関数定義時に一度だけ評価される。つまり、空のリストオブジェクトが最初に一つだけ作られ、その後は毎回同じリストが参照される。そのため、関数を呼ぶたびに要素が蓄積されていく。

なぜこのような仕様なのか

Python の関数はファーストクラスオブジェクトであり、関数自体がオブジェクトとして存在する。デフォルト引数は関数オブジェクトの属性 __defaults__ に格納されており、関数定義時に一度だけ計算されて保持される。

def append_to(element, target=[]):
    target.append(element)
    return target

print(append_to.__defaults__)  # ([],)
append_to(1)
print(append_to.__defaults__)  # ([1],)
append_to(2)
print(append_to.__defaults__)  # ([1, 2],)

__defaults__ を確認すると、デフォルト引数のリストが関数呼び出しのたびに変化していることがわかる。これは同じオブジェクトが共有されている証拠である。

この仕様には合理的な理由がある。もし毎回デフォルト引数を評価するなら、関数が呼ばれるたびに式を実行するコストがかかる。定義時評価であれば、そのコストは一度きりで済む。また、ミュータブルなデフォルト引数をキャッシュとして意図的に使うテクニックも存在する(後述)。

ミュータブルなデフォルト引数を避ける方法

リストや辞書のようなミュータブルなオブジェクトをデフォルト引数にしたい場合、None を使うイディオムが標準的である。

def append_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [2]
print(append_to(3))  # [3]

target=None とし、関数の冒頭で None かどうかを判定して新しいリストを作成する。これにより、呼び出しごとに独立したリストが生成される。

Python 3.10 以降では、| 演算子を使った型ヒントとの組み合わせも自然に書ける。

def append_to(element: int, target: list[int] | None = None) -> list[int]:
    if target is None:
        target = []
    target.append(element)
    return target

ミュータブルなデフォルト引数を意図的に使う例

この「一度だけ評価される」性質を逆手にとって、関数にキャッシュを持たせることができる。たとえばメモ化を簡易的に実装する場合である。

def fibonacci(n, cache={0: 0, 1: 1}):
    if n not in cache:
        cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return cache[n]

print(fibonacci(10))  # 55
print(fibonacci(50))  # 12586269025

cache 辞書は関数定義時に一度だけ作成され、以降の呼び出しで共有される。そのため、一度計算した値は cache に残り、再計算を避けられる。ただし、この手法はコードの可読性を下げ、意図が伝わりにくいため、実務では functools.lru_cache を使うほうが望ましい。

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

イミュータブルなデフォルト引数は問題にならない

整数、文字列、タプルなどのイミュータブルなオブジェクトをデフォルト引数にする場合、この問題は発生しない。

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))  # Hello, Alice!
print(greet("Bob"))    # Hello, Bob!

文字列 "Hello" はイミュータブルなので、関数内で変更されることはない。したがって、何度呼び出しても同じ文字列が返される。問題が起きるのは、デフォルト引数がミュータブルで、かつ関数内でそのオブジェクトを変更する場合に限られる。