Pythonは参照渡し、だが。。。
Pythonでは関数に値を渡す時にその参照が渡されます。
ですが、渡された関数内で変更されるとき、 渡した値自体が変更されるかどうかは 渡されたオブジェクトのタイプによります。
オブジェクトのタイプはImmutable(変更不可)とMutable(変更可能)に分けられます。
- Immutable: int, float, str, tuple 等
- Mutable: list, set, dict 等
このうち、Mutableな値が関数に渡された場合、 中で変更すると値の変更が外にも反映されますが、 Immutableなものに関しては変更されません。
ただ、これもちょっと注意する必要があって、 listでもlist内の項目を変更したり値を追加したりするのではなく、 新たなlist自体を与える様な事をする場合は外では書き換えられません。
まず、intについて次の様なテストをしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
結果は
id of 1: 140469208133032
id of 2: 140469208133008
Input variables
test_i: 1 140469208133032
Input variables in the function
i: 1 140469208133032
After change
i: 2 140469208133008
After the function
test_i: 1 140469208133032
こんな感じ。
まず、Pythonではオブジェクトでなくても、
1
とか値が呼び出されるときにもそのためのメモリが確保さあれています。
なので、最初に1
が140469208133032
になっていて、
a=1
によってa
がこの場所を示す様になっています。
これを関数の中に入れると中でも同じIDを持ってる事が分かります。
その後、2
を入れると2
のIDを持つ様になりますが、
関数の外に行くとa
は1を指したまま。
次に、リストについて。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
こんな感じ。関数内では[]
やappend
を使ってリストを変更しています。
これを実行すると
id of [1, 2, 3]: 4340072032
id of [4, 5, 6]: 4340072032
Input variables
test_l: [1, 2, 3] 4340072032
Input variables in the function
l: [1, 2, 3] 4340072032
After change
l: [4, 2, 3, 5] 4340072032
After the function
test_l: [4, 2, 3, 5] 4340072032
id of [1, 2, 3]: 4340305288
こんな感じ。 まず、最初にリストそのもののIDを見ていますが、 中身を変えても同じ物を返します。
これはこの時点でlocalな値として作られているリストがあって、 同じ物を使っている、と言う事。
それをtest_l
に代入するとそのリストの場所はtest_l
のものになります。
関数に入れると同じIDの物が入っていて、中で変更すると 関数の外にも反映されます。
さらに、最後で再び[1, 2, 3]
とオブジェクトに結びついてないものを見ると
新たなIDが割り振られています。
この様にint型だと中での変更は外に反映されないが、 list型だとされる、という感じなのですが、次の様な状況があるので 注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
この様にlistに新たなリストを代入すると
Input variables
test_l: [1, 2, 3] 4471934560
Input variables in the function
l: [1, 2, 3] 4471934560
After change
l: [4, 5, 6] 4472167816
After the function
test_l: [1, 2, 3] 4471934560
となって関数内での変更は関数外には反映されません。
つまり、MutableであろうとImmutableであろうと 新しい値を入れた場合には関数の外側には反映されませんが、 Mutableの場合には中身を変更できるのでその中身の変更は 外でも反映される、ということ。
通常のコピー時
これらは何も関数の引数に限るものではなくて、通常の値のコピーでも同じことです。
int型とlist型のコピーを見てみると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
こんな感じでlistでコピーした先で中の値を変更すると 元の値も変更されます。
リストで元の値を変更しないようにするには
b = list(b)
の様に新たなリストとして作って上げればOK。
1 2 3 4 5 6 7 |
|
ただ、これも多階層になってくると単純には行きません。
1 2 3 4 5 6 7 8 |
|
と、2重になってる内側のリストを変更しようとすると元の値が変更されてしまいました。
もし、元の値を変更したくない場合には深いコピー(deepcopy)を使います。
1 2 3 4 5 6 7 8 |
|
と変更が元に反映されません。
ちょっとIDを見てみると
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
な感じで普通のコポーでは全てが同じID、
list
を使ったものでは中のリストは同じID、
最後のdeepcopy
は中のリストも別のIDで別物になっていることが分かります。
デフォルト引数の罠
関数の引数に関して、もう一つ、 デフォルト引数を与える時に結構見逃しがちな罠があります。
Pythonのデフォルト引数は
1 2 |
|
みたいにするとデフォルト引数を指定できて、 この関数には引数として値を与えなくてもデフォルトの値が使われる様になりますが、 このデフォルトの値は関数が初めて使われた時に初期化されて以降、 値が保存されます。
ちょっと分かりづらいですが、 Immutableなオブジェクトをデフォルト引数に使う場合は特に注意する必要はありません。
変更不可なので最初にa=1
となったらその後このa
は常に1
になります。
ですが、Mutableなlist等にデフォルト引数を使うと、 その値を変更してしまうとデフォルトの値が変わります。
1 2 3 4 5 6 7 8 9 10 11 |
|
の様に、関数のデフォルト引数の値を外に返すだけの関数ですが、 そのリストを変更すると、次に関数を呼ぶ時にそのデフォルト引数も変更されてる事が分かります。
勿論、関数の中でa.append(4)
などして変更した場合にもその次の時から
変更されたa
がデフォルトの値になります。
これを避けるためには、Mutableなものをデフォルト引数として使わないか、
使う場合には
中で1回list(a)
なり必要ならdeepcopy
を使って
元に影響しない物を用意してから使うか、
1 2 3 4 |
|
の様にデフォルト引数としてはNone
とかを指定しておいて、
関数の中でNone
のままなら決まった値にする、みたいな事をするか。
通常そのまま渡されたリストを変更しようとは余り思わないかもしれませんが、
コピーしたつもりでも普通にa = b
としただけだったり
多階層のlistやdictの場合にdeepcopy
を使わずにコピーすると
思わずデフォルト引数の値を変更してしまうかもしれないので注意が必要です。