Python関数のデフォルト引数の注意点

Python関数のデフォルト引数の注意点

ミュータブルな型とイミュータブルな型

Pythonは全ての型が参照渡しになっています。
ただし、イミュータブルな型のものはデータが更新されると新たなメモリ領域を確保する仕組みになっています。
つまり、何らかデータの変更が行われた段階で新たなメモリ領域を確保して参照するようになります。
Pythonの変数名は値にラベル付をしたものと考えると良いです。イミュータブルな型ではラベルが付け替えられることになります。

ミュータブルな型にはリスト型、辞書型、集合型などがあります。

int、float、文字型、タプル型などイミュータブルなデータが更新されると、新たなメモリ領域を確保して元の値は変更されない結果になります。

ざっくりと覚えるには、タプル以外のリストの仲間がミュータブルでそれ以外はイミュータブルと覚えておけば良いです。
そしてミュータブルなこのリストの仲間はこれから説明する少し面倒なことが起こることを知っておきましょう。
なお、id()関数はオブジェクトのid番号を調べることができます。ミュータブルなオブジェクトに慣れるまで、id()を使ってその値のid番号を確認して見るとよいです。

次に、ミュータブルな型の問題点を確認してみます。

デフォルト引数の注意点

引数のデフォルト値にミュータブルなリストを指定することもできますが注意が必要です。
また、空のリストをデフォルト引数で指定することができます。この方法は結構便利に使えます。

次の例は1から10までの整数の数列を作る関数です。

def create_int_list(numbers=[]):
  for i in range(10):
    numbers.append(i)
  return numbers

numbers = create_int_list()
print(numbers)

結果
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

特に問題なく数列が作られました。
けれども次のように複数回この関数を実行すると困ったことになります。

numbers = create_int_list()
print(numbers)
numbers2 = create_int_list()
print(numbers2)
numbers3 = create_int_list()
print(numbers3)

結果
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

初期化したはずの空のリストに要素が追加されてしまいます。

これは、リストの値が初期化されるのではなく追加されまた結果です。
つまり、新たに関数を実行すると引数のリストは新たなメモリを確保するのではなく、前回使用したメモリに追加することになるのです。
ミュータブルな型を使う場合このように思わぬ落とし穴が存在しますので十分に注意する必要があります。

さて、この現象を解決するには、次のように引数をつくのではなく、ローカル変数として宣言するのが簡単です。

def create_int_list():
  numbers=[]
  for i in range(1,11):
    numbers.append(i)
  return numbers

numbers = create_int_list()
print(numbers)
numbers2 = create_int_list()
print(numbers2)
numbers3 = create_int_list()
print(numbers3)

結果
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

ローカル変数は、関数内でしか生存しませんので、新たに関数を実行すると新たな場所にリストの値が入るようになり上のような問題が起こりません。

引数にミュータブルな型を使う方法

どうしても引数にミュータブルな型を使いたい場合は、次のようにします。
これはよく使われるテクニックですので覚えておくとよいでしょう。

def create_int_list(numbers=None):
  if numbers is None:
    numbers=[]
  for i in range(1,11):
    numbers.append(i)
  return numbers

numbers = create_int_list()
print(numbers)
numbers2 = create_int_list()
print(numbers2)
numbers3 = create_int_list([-3,-2,1])
print(numbers3)
numbers4 = create_int_list([-3,-2,1])
print(numbers4)

結果
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[-3, -2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[-3, -2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

仮引数に直接リストを入れるのではなく、Noneを代入した変数を使います。
実引数に何も指定をしないとNoneが代入されifの中でローカル変数として初期化されますので、何度関数を実行しても要素が追加されることはありません。

また、何らかリストの値を実引数に指定すると、そのままfor文でその値が使われることになり、これも問題が起こりません。
これは次のことと同じです。これはリストの要素を明示的に初期化しています。この場合は問題なく初期化されます。

numbers = create_int_list()
numbers = create_int_list([])

実引数に[]を入れるか入れないかの違いは大きいです。
実引数に何も指定しない場合は、デフォルト引数が使用されます。ミュータブルな型ではデフォルト引数の値は次に関数を実行した時に引き継がれます。そのため今回のような問題が起こりました。

けれども、実引数に空のリスト[]を指定した場合は、引数の要素を変更するのではなくリストの値そのものを変更することになりますので、id番号は変えられて別の領域に保存されます。

ミュータブルなリストはリストの値の中の要素を変更することができます。この場合id番号は引き継がれます。
けれども、リストの値そのものを変更する場合は、これはid番号は変更されて保存されます。
紛らわしいので勘違いしないように注意しましょう。

リストの値を再代入した場合の例

# ミュータブルな例
l = [1,2,3] 
print(id(l),l) 
l = [5,6]
print(id(l),l) 

結果
140062104816136 [1, 2, 3]
140062104817352 [5, 6] # id番号が変わった

リストの要素を追加した場合の例

# ミュータブルな例
l = [1,2,3]
print(id(l),l)
l.append(4)
print(id(l),l)

結果
139874899366024 [1, 2, 3]
139874899366024 [1, 2, 3, 4] # id番号が変わらない