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の取り扱いには多少気を払い、安全なコードを書いていきたいですね。