#はじめに
「pythonの参照について」という記事が興味深かったのでメモ。
この記事に、
pythonを始めたころに、一番につまづいたのが、いたるところでよく見られる「pythonの引数は全て参照渡しである」という謎の説明である。
という一節があった。
すでにコメントでも指摘されているように、より正確には「Pythonの引数は全て参照の値渡し」ということなのだけども、それはともかくとして、Pythonには値渡しが存在しないらしい!
以前自分もほぼ同じテーマで「値渡しと参照渡しと参照の値渡しと」という記事を書いていて、そこでは整数や浮動小数点数はPythonだと値渡しと書いていた。
実際それで矛盾ない説明が得られていたし、元記事の
変数に新たなオブジェクトそのものが代入されるときには、これまでの参照先から変わって新たなオブジェクトが作られた場所を参照するようになる。
という部分も自分が参照の値渡しについての説明で書いたものと同じだ。
でもコメントとかを見る感じちょっと不正確なことを書いてしまったかもしれない、、、ということで詳しく調べてみました。
#PythonとC++での整数の挙動
以下のPythonのコードを考えてみる。
a = 1
b = a
print("a : value = " + str(a) + "\tid = " + str(id(a)))
print("b : value = " + str(b) + "\tid = " + str(id(b)))
a : value = 1 id = 4360526320
b : value = 1 id = 4360526320
これは確かに「参照の値渡し」であって「値渡し」ではない。なぜなら値渡しならば以下のC++のようにid(ポインタ値)は一致しないはずだからだ。
#include <iostream>
int main(){
int a = 1;
int b = a;
std::cout << "a : value = " << a << "\tid = " << &a << "\n";
std::cout << "b : value = " << b << "\tid = " << &b << "\n";
return 0;
}
a : value = 1 id = 0x7fff50360ae8
b : value = 1 id = 0x7fff50360ae4
次にこうした場合はどうなるだろう。
a = 1
b = a
print("a : value = " + str(a) + "\tid = " + str(id(a)))
print("b : value = " + str(b) + "\tid = " + str(id(b)) + "\n")
b = 2
print("a : value = " + str(a) + "\tid = " + str(id(a)))
print("b : value = " + str(b) + "\tid = " + str(id(b)))
a : value = 1 id = 4348889584
b : value = 1 id = 4348889584
a : value = 1 id = 4348889584
b : value = 2 id = 4348889616
#include <iostream>
int main(){
int a = 1;
int b = a;
std::cout << "a : value = " << a << "\tid = " << &a << "\n";
std::cout << "b : value = " << b << "\tid = " << &b << "\n\n";
b = 2;
std::cout << "a : value = " << a << "\tid = " << &a << "\n";
std::cout << "b : value = " << b << "\tid = " << &b << "\n";
return 0;
}
a : value = 1 id = 0x7fff561cdae8
b : value = 1 id = 0x7fff561cdae4
a : value = 1 id = 0x7fff561cdae8
b : value = 2 id = 0x7fff561cdae4
このようにC++と違ってPythonだと別の値を代入するとidも変化する。つまり参照先が変わる。
+=のような複合代入だとどうだろう。
a = 1
b = a
print("a : value = " + str(a) + "\tid = " + str(id(a)))
print("b : value = " + str(b) + "\tid = " + str(id(b)) + "\n")
b += 2
print("a : value = " + str(a) + "\tid = " + str(id(a)))
print("b : value = " + str(b) + "\tid = " + str(id(b)))
a : value = 1 id = 4507830768
b : value = 1 id = 4507830768
a : value = 1 id = 4507830768
b : value = 3 id = 4507830832
これも同じだ。
元記事のコメントにもあるように、b += 2
というのは実際にはb = b + 2
が呼ばれていて、やはり新しい参照先が代入されている。
つまりPythonでは「整数の値を変更するとそのたびにメモリ上に新しいオブジェクトが作られ、それが代入されるためidも変化する」ということになる。
これは言い換えると、Pythonでは「一度メモリ上に整数を生成すると、それを変更することはできない」ということであり、これを「イミュータブル(immutable)」というらしい。逆は「ミュータブル(mutable)」。
他にイミュータブルな型としては、浮動小数点数、ブーリアン、文字列、タプル、Noneなどがあり、Pythonで本質的になってくるのは「値渡しか否か」ではなく「ミュータブルか否か」ということになる。うーん知らなかった。
#Javaの文字列とラッパークラス
ところで、上のPythonの例と似たものがJavaにもある。StringおよびIntegerやDoubleなどのラッパークラスだ。
class Ex1{
public static void main(String[] args){
String s1 = "foo";
String s2 = s1;
System.out.println("s1 : " + s1);
System.out.println("s2 : " + s2 + "\n");
s2 += "bar";
System.out.println("s1 : " + s1);
System.out.println("s2 : " + s2 + "\n");
Integer i1 = 1;
Integer i2 = i1;
System.out.println("i1 : " + i1);
System.out.println("i2 : " + i2 + "\n");
i2 += 2;
System.out.println("i1 : " + i1);
System.out.println("i2 : " + i2);
}
}
s1 : foo
s2 : foo
s1 : foo
s2 : foobar
i1 : 1
i2 : 1
i1 : 1
i2 : 3
このように、JavaのStringやIntegerは参照型にもかかわらず、値渡しのような挙動をしている。
試しに上をコンパイルしてできたclassファイルをjadを使ってデコンパイルしてみよう。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Ex1.java
import java.io.PrintStream;
class Ex1
{
Ex1()
{
}
public static void main(String args[])
{
String s = "foo";
String s1 = s;
System.out.println((new StringBuilder()).append("s1 : ").append(s).toString());
System.out.println((new StringBuilder()).append("s2 : ").append(s1).append("\n").toString());
s1 = (new StringBuilder()).append(s1).append("bar").toString();
System.out.println((new StringBuilder()).append("s1 : ").append(s).toString());
System.out.println((new StringBuilder()).append("s2 : ").append(s1).append("\n").toString());
Integer integer = Integer.valueOf(1);
Integer integer1 = integer;
System.out.println((new StringBuilder()).append("i1 : ").append(integer).toString());
System.out.println((new StringBuilder()).append("i2 : ").append(integer1).append("\n").toString());
integer1 = Integer.valueOf(integer1.intValue() + 2);
System.out.println((new StringBuilder()).append("i1 : ").append(integer).toString());
System.out.println((new StringBuilder()).append("i2 : ").append(integer1).toString());
}
}
少し見づらいが、二つとも複合代入が新しいオブジェクトを作って参照先を変更する処理に書き換えられている。
自分がまだこのことを知らない頃、「メソッドで複数のintやdoubleを返したいけどそのためだけにクラスつくるのは面倒だし、intやdoubleは値渡しにしかできないし、、、あっでもラッパークラスで引数に渡せばいけるんじゃね?」と思って見事失敗しました。
#まとめ
以上をまとめると、イミュータブルな型では一度生成したオブジェクトの値を変更することができないので、参照の値渡しであっても値渡しと実質的にほぼ同じ挙動になる、という結論になる。
いやはや失礼しました。