[python] デフォルト引数にmutable objectを指定してはいけない

 pythonでは、デフォルト引数という便利な機能が用意されています。他の言語ではオプショナル引数などと呼ばれているような機能で、引数を設定しない場合に使われるデフォルトの引数を設定できる、というものです。

def sample(arg_a="My name is", arg_b="python"):
  print(f"{arg_a} {arg_b}")

sample() # -> My name is python
sample(arg_a="Your name is") # -> Your name is python
sample(arg_b="Tython") # -> My name is Tython

 通常なら便利な使い方で問題ありませんが、ここにmutable object(listやdictなど)を指定すると思わぬバグに遭遇します。

参照の罠

 デフォルト引数は、関数が呼ばれる度に毎回生成されるわけではありません。最初の1回のみ生成され、あとは使い回されます。ここに変更可能なオブジェクトを指定するとどうなるでしょうか。試してみましょう。

# 引数"number_list"がなければ空のリストを生成し、あればそれに第一引数を追加します。
def append_number(a, number_list=[]):
  number_list.append(a)
  return number_list

number_list = append_number(0)
number_list = append_number(1, number_list)

print(number_list) # -> [0, 1]

number_list2 = append_number(0)
number_list2 = append_number(1, number_list2)

print(number_list2) # -> [0, 1, 0, 1]

 この関数を使う側が関数のコメントを信じてしまった結果、別々のリストを想定しているはずなのに、なぜか以前の結果と連結されたリストが出てきてしまいます。

解決方法

 多少スマートでなくても、内部でちゃんと生成しましょう。

# 引数"number_list"がなければ空のリストを生成し、あればそれに第一引数を追加します。
def append_number(a, number_list=None):
  if number_list is None:
    number_list = []
  number_list.append(a)
  return number_list

number_list = append_number(0)
number_list = append_number(1, number_list)

print(number_list) # -> [0, 1]

number_list2 = append_number(0)
number_list2 = append_number(1, number_list2)

print(number_list2) # -> [0, 1]

immutable objectは問題なし

 strなどの変更不可能なオブジェクトは全く問題ありません。変更されなければ副作用は起きないからです。なので、デフォルト引数にstrやintを設定するのは可読性の面からも推奨されます。

余談

 そもそも、mutable objectを引数にとり、それを加工するなどやるべきではありません。コードの見通しが非常に悪くなるからです。そういった場合は、クラスにしてフィールドとして持ってしまいましょう。

class NumberAppender:

  def __init__(self):
    self._number_list = []

  def append(self, number):
    self._number_list.append(number)

  @property
  def result(self):
    return self._number_list

appender = NumberAppender()
appender.append(0)
appender.append(1)
appender.append(2)
print(appender.result) # -> [0, 1, 2]

 ただ、resultによって返されたlistも普通に変更できてしまうので、この手のクラスを作る場合は、resultをコールされたらappendを無効化する、そもそもimmutableなオブジェクトであるtupleを返す、などとするべきかもしれません。

まとめ

 初歩的なバグですが、やってしまいがちです。mutable objectの取り扱いには多少気を払い、安全なコードを書いていきたいですね。