なぜこんな記事を?
pythonには、割と理解し難い仕様が含まれていますが、引数のデフォルト値にリストなどを指定した場合の問題は、特に理解しづらいようです。
私の持っている書籍には、この問題に対して1ページ使いながら、「ある種グローバルな変数を参照することになり...」という説明をしています。しかしこの説明は間違ってはいませんが、不適当です。
何が起きるのか
引数のデフォルト値にリストを指定したら、何が起きるかをサンプルで示します。
def func(a, b=[1]):
b[0] += a
print(b[0])
func(1) # --> 2
func(1) # --> 3
同じ引数で2回実行すると、結果が変わりました。呼び出す度に結果が異なる関数が出来てしまいました。
より複雑なコードでこの様なバグを作ってしまうと見つけづらいと思われます。例えば、引数bを指定する場合としない場合が混在しているケースでは、バグの原因を特定するのに時間がかかるかも知れません。
動作
どうして上記の様な結果になったのでしょうか。
ポイントは、def文が宣言文ではなく、他の要素と同様に命令文であるということです。def文は、関数型のオブジェクトを生成し、それに名前をつけて格納する命令文です。
そして、引数のデフォルト値は、遅延評価されず、def文の実行時に行われます。
ここにpythonにおけるリストなどのimmutableな型の値の扱いが関わってきます。
上記の例では、[1]
を格納した無名のリストが生成され、その参照が変数bのデフフォルト値とされる様になります。
変数bが引数で与えられなかった場合、2行目のコードは、def文を実行時に[1]
を格納していた無名のリストの中身を書き換えます。このために、同じ引数を与えて実行した2回目の結果が変わるのです。
この仕様を理解すべきことは、def文は実行文で、デフォルト値の評価はdef文実行時に行われるということです。
対処方法
この仕様への対処方法は、下記のようにデフォルト値にimmutableな値を与えないことです。
def func2(a, b=None):
if b is None:
b = [1]
b[0] += a
print(b[0])
func2(1) # --> 2
func2(1) # --> 2
その他の仕様の理解のヒント
pythonで分かり難い仕様は、インタプリタとしてどう動作するのかを想像すると、理解し易いことが多いです。
また、デコレータの様なSyntax sugar (構文糖衣)な仕様については、同等の動作をするコードを知り、それを簡単に描けるようにしているだけだと考えた方が理解し易いかと思います。
下に示した参考図書も、分かり難い仕様の理解に役立つものです。
参考ページ、図書
-
『python文法詳解』(オライリー)