概要
Pythonで関数の受け渡しが参照渡しとされているが、改めて変数とオブジェクトの関係を含めて確認してみた。
オブジェクトの参照に関する基本的な考え方は
- 論理値や数値も含めて全てオブジェクト
- 変数にはインスタンスのアドレスが格納され、参照される
- 数値や文字列などイミュータブルなオブジェクトの場合、リテラルの表現が同じものは共通の1つのインスタンスとなる
- リストなどミュータブルなオブジェクトは、インスタンスの内容が変更されても参照先は変わらない(逆に言えば、参照先が変わらないのに内容が変更されている可能性がある)
関数の引数の受け渡しは
- 仮引数は、関数が呼び出し時には元のオブジェクトへの参照を指しているが、イミュータブルオブジェクトが変更された場合は参照先が変わり、呼び出し元の引数に影響を与えない
- 引数の参照先がミュータブルオブジェクトの場合、インスタンスの内容が変更されても参照先は変わらず、呼び出し元の内容も変更される
変数からオブジェクトへの参照
数値の場合
下記のコードをまず確認する。
1 2 3 4 5 6 7 8 9 10 |
a = 1 b = a c = b print('a={}, b={}, c={}'.format(a, b, c)) print('$a={}, $b={}, $c={}'.format(id(a), id(b), id(c))) # a=1, b=1, c=1 # $a=1943168176, $b=1943168176, $c=1943168176 # 3つの変数の値は等しく、参照番地は同じ |
ある数値を変数に代入すると、その変数には数値(オブジェクト)のアドレスがセットされる。その変数の内容(アドレス)を別の変数に代入すると、新しい変数も同じアドレスをさすようになる。
次に、同じ数値を指している変数の一つに別の数値を代入すると、その変数は新しい数値オブジェクトのアドレスを指すようになる。
1 2 3 4 5 6 |
b = 2 # a=1, b=2, c=1 # $a=1943168176, $b=1943168192, $c=1943168176 # bに新しい値をセットすると、参照番地も変わる # a, cの値、参照番地は変化しない |
以下のように、同じ値の数値は1つのオブジェクトのアドレスが共有される。
1 2 3 4 5 |
c = 2 # a=1, b=2, c=2 # $a=1943168176, $b=1943168192, $c=1943168192 # cにbと同じ値をセットすると、値・参照番地とも同じになる |
数値計算の場合も、結果が同じ値なら1のオブジェクトが共有される。
1 2 3 4 5 6 |
a = 3 * 4 b = 2 * 6 # a=12, b=12 # $a=1943168352, $b=1943168352 a# 計算過程に関わらず、計算結果が同じなら同じ番地を参照 |
変数が絡む演算でも、結果が同じ値なら同じアドレスを指す。
1 2 3 4 5 6 7 |
x = 3 a = x * 4 b = 2 * 2 * x # a=12, b=12 # $a=1943168352, $b=1943168352 # 変数が絡む場合でも計算結果が同じならオブジェクトも同じ |
浮動小数点の場合、精度上少しでも異なる値は違うオブジェクトになる。
1 2 3 4 5 6 |
a = 1.0 / 3 b = 10 / 3 - 3 # a=0.3333333333333333, b=0.3333333333333335 # $a=62840080, $b=62839984 # 浮動小数点の場合、必ずしも予想した同じ値とならない場合がある |
文字列の場合
数値の場合と同じで、変数は文字列オブジェクトのアドレスを指す。
1 2 3 4 5 6 |
a = 'AA' b = a # a=AA, b=AA # $a=67099680, $b=67099680 # 同じ文字列オブジェクトを参照 |
異なる内容の文字列は、異なるオブジェクトとなる。
1 2 3 4 5 |
b = 'AB' # a=AA, b=AB # $a=67099680, $b=67099776 # 新たな文字列は新たなオブジェクトとして生成され参照される |
同じ内容の文字列リテラルは、異なる位置で用いられても1つのオブジェクトとして共有される。
1 2 3 4 5 |
a = 'AB' # a=AB, b=AB # $a=67099776, $b=67099776 # 同じ内容の文字リテラルは同じオブジェクトとして参照される |
面白いことに、文字列リテラル同士の演算結果が同じなら、これも同じオブジェクトとして共有される。
1 2 3 4 5 |
b = 'A' + 'B' # a=AB, b=AB # $a=67099776, $b=67099776 # 文字列リテラルの演算結果が同じ内容なら同一のオブジェクト |
同じオブジェクトを指していても、一方に演算を施すと新たなオブジェクトが生成されるため、異なるオブジェクトを指すようになる。
1 2 3 4 5 |
a += 'C' # a=ABC, b=AB # $a=67172064, $b=67099776 # 文字列に変更があると新たな別のオブジェクトになる |
なお、リテラル同士の演算で結果が同じ場合はオブジェクトが共有されたが、変数が絡む場合は、内容が同じであっても異なるオブジェクトとなる。
1 2 3 4 5 6 7 |
x = 'A' a = x + 'B' b = x + 'B' # a=AB, b=AB # $a=67172608, $b=67172064 # 結果が同じでもリテラルのみでない演算の場合は異なるオブジェクト |
リストの場合
リストの場合もオブジェクトへの参照が変数に保存される。
1 2 3 4 5 6 |
a = [0, 1, 2] b = a # a=[0, 1, 2], b=[0, 1, 2] # $a=66799096, $b=66799096 # 同じリストを参照 |
リストの要素を変更した場合、そのリストを指している全ての変数に変更結果が反映される。
1 2 3 4 5 |
a[0] = 1 # a=[1, 1, 2], b=[1, 1, 2] # $a=66799096, $b=66799096 # 同じリストを参照しているので変更結果も参照先に反映 |
注意点。リストの場合、リテラルが同じでも異なるオブジェクトが生成される。
1 2 3 4 5 6 |
a = [0, 1, 2] b = [0, 1, 2] # a=[0, 1, 2], b=[0, 1, 2] # $a=67159624, $b=67158704 # リストの場合、同じリテラル表現でも異なるオブジェクトになる |
オブジェクトが異なるため、片方の変更は他方に反映されない。
1 2 3 4 5 6 |
a[0] = 1 # a=[1, 1, 2], b=[0, 1, 2] # $a=67159624, $b=67158704 # リストの内容を変化させても参照番地は変わらない # 異なるオブジェクトのため、変更は波及しない |
関数の引数の参照
数値の場合
関数の引数に数値を渡す場合の流れは以下の通り。
- 仮引数は引数と同じオブジェクトを指す
- 関数内で引数が変更されると、新たなオブジェクトを指すようになる
- その結果、仮引数に渡した変数は変更されない
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def func_num(arg): print('arg_before={}, id={}'.format(arg, id(arg))) arg +=1 print('arg_after={}, id={}'.format(arg, id(arg))) return arg var = 1 print('variable={}, id={}'.format(var, id(var))) result = func_num(var) print('result={}, id={}'.format(result, id(result))) print('variable={}, id={}'.format(var, id(var))) # variable=1, id=1943037104 # arg_before=1, id=1943037104 # arg_after=2, id=1943037120 # result=2, id=1943037120 # variable=1, id=1943037104 # 引数は、数値への参照で渡される # 関数内で異なる値にセットされると引数の参照番地が変わる # しかし、その結果は呼び出し元の実引数に影響しない |
文字列の場合
文字列の場合も、関数内での変更は呼び出し元に影響を与えない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def func_str(arg): print('arg_before={}, id={}'.format(arg, id(arg))) arg += 'X' print('arg_after={}, id={}'.format(arg, id(arg))) return arg var = 'ABC' print('variable={}, id={}'.format(var, id(var))) result = func_str(var) print('result={}, id={}'.format(result, id(result))) print('variable={}, id={}'.format(var, id(var))) # variable=ABC, id=72144960 # arg_before=ABC, id=72144960 # arg_after=ABCX, id=72220064 # result=ABCX, id=72220064 # variable=ABC, id=72144960 # 数値の場合と同じで、引数は参照渡しだが呼び出し元の変数は変更されない |
リストの場合
リストを引数に渡した場合、関数内での変更が呼び出し元にも影響を与える。リストのようなミュータブルオブジェクトの場合、それに対する変更は元のインスタンスに対する変更であり、イミュータブルなオブジェクトのように新しいインスタンスが生成されるわけではないため。
元のインスタンスに影響を波及させたくない場合はコピー、ディープコピーを使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def func_list(arg): print('arg_before={}, id={}'.format(arg, id(arg))) arg.append(0) print('arg_after={}, id={}'.format(arg, id(arg))) return arg var = [3, 2, 1] print('variable={}, id={}'.format(var, id(var))) result = func_list(var) print('result={}, id={}'.format(result, id(result))) print('variable={}, id={}'.format(var, id(var))) # variable=[3, 2, 1], id=71845368 # arg_before=[3, 2, 1], id=71845368 # arg_after=[3, 2, 1, 0], id=71845368 # result=[3, 2, 1, 0], id=71845368 # variable=[3, 2, 1, 0], id=71845368 # リストの場合は内容が変化しても参照は変更されないので # 呼び出し元の変数が影響を受ける |