LoginSignup
11
9

More than 5 years have passed since last update.

参照についてもう少し詳しく ~PythonとJavaを例に~

Posted at

はじめに

pythonの参照について」という記事が興味深かったのでメモ。
この記事に、

pythonを始めたころに、一番につまづいたのが、いたるところでよく見られる「pythonの引数は全て参照渡しである」という謎の説明である。

という一節があった。
すでにコメントでも指摘されているように、より正確には「Pythonの引数は全て参照の値渡し」ということなのだけども、それはともかくとして、Pythonには値渡しが存在しないらしい!
以前自分もほぼ同じテーマで「値渡しと参照渡しと参照の値渡しと」という記事を書いていて、そこでは整数や浮動小数点数はPythonだと値渡しと書いていた。
実際それで矛盾ない説明が得られていたし、元記事の

変数に新たなオブジェクトそのものが代入されるときには、これまでの参照先から変わって新たなオブジェクトが作られた場所を参照するようになる。

という部分も自分が参照の値渡しについての説明で書いたものと同じだ。
でもコメントとかを見る感じちょっと不正確なことを書いてしまったかもしれない、、、ということで詳しく調べてみました。

PythonとC++での整数の挙動

以下のPythonのコードを考えてみる。

ex1.py
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(ポインタ値)は一致しないはずだからだ。

ex1.cpp
#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

次にこうした場合はどうなるだろう。

ex2.py
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
ex2.cpp
#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も変化する。つまり参照先が変わる。
+=のような複合代入だとどうだろう。

ex3.py
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などのラッパークラスだ。

Ex1.java
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を使ってデコンパイルしてみよう。

Ex1.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は値渡しにしかできないし、、、あっでもラッパークラスで引数に渡せばいけるんじゃね?」と思って見事失敗しました。

まとめ

以上をまとめると、イミュータブルな型では一度生成したオブジェクトの値を変更することができないので、参照の値渡しであっても値渡しと実質的にほぼ同じ挙動になる、という結論になる。
いやはや失礼しました。

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9