大阪大学医学部 Python会

Now is better than never.

Pythonの変数と代入について

2018-12-20(Thu) - Posted by 小川 in 技術ブログ    tag:Python

Contents

    Pythonの変数、ふだん何気なく使っていますが、やっていることは実は結構複雑です。主にC/C++と対比しつつ簡単にまとめてみます。

    実際のところ、よく知らなくてもあまり影響がない場合が殆どです。が、複雑な操作を行ったり何か変数の挙動が不審な場合などは、思い出してみるのもよさそうです。

    変数は参照である

    一言でいえば表題の通り。 Pythonでの変数への代入とは、変数の参照するメモリ(インスタンス)を切り替えることであり、メモリの内容を書き換えることではない。これが多くの言語(C/C++など)と大きく違うところです。

    変数の代入(1)

    例えばこんなコード。

    # Python3
    a = 0
    a = 1
    print(a)  # 1を出力
    

    C++で書き換えると、

    // C++
    int a = 0;
    a = 1;
    cout << a << endl;  // 1を出力
    

    なのか?? (#include\とかint main(void){...}とかは全部省略。)

    実は違うのです。
    C++の方ではaで表されるメモリ領域を一つ確保し、そこにまず0をセット、次に1を、同じメモリ領域を書き換えてセットしています。

    Pythonの方はそうではありません。まず0の値を持つメモリ領域を確保し、変数aがそこを指すようにします。次に1の値を持つ別のメモリ領域を確保し、変数aがそこを指すように切り替えます。

    C++で無理矢理それっぽいものを書くと、

    // C++
    int *a = new int(0);
    a = new int(1);
    cout << *a << endl;  // 1を出力
    

    のようになります。
    (C++に参照定数はあるが参照変数がないため、ポインタで書いた。)

    実はこのC++コード、2行目で確保したint(0)メモリを3行目のint(1)代入時に放棄していて、メモリリークが起こっています。C++では、これを処理(メモリ解放)するためのコードを本当は書き加えないといけません。
    しかし、Pythonではそれをガベージコレクタという仕組みが勝手に代行してくれます。逆にこれがないと、Pythonではメモリリークが頻繁に起こって大変なことになります。

    変数の代入(2)

    問題です。次のコードは何を出力するでしょうか。

    # Python3
    a = 0
    b = a
    a = 1
    print(b)  # 何が出る?
    

    答えは「0です。
    C++で書いた似たようなコード、

    // C++
    int a = 0;
    int b = a;
    a = 1;
    cout << b << endl;  // 出力は「0」
    

    も結果は同じなのですが、内部動作は全く異なります。Pythonでの変数代入は参照の切り替えであることがやはりポイントです。

    一方で、過程は違えどPythonもC++も結果は変わりません。気にしなくても大抵はうまくいく、というのも大事なところです(笑)。

    関数引数はすべて参照渡し

    「変数が全て参照」であることから、関数引数もまた参照渡しとなります。C/C++のデフォルトである値渡しと異なり、メモリのコピーなどは行われません。

    では再び問題。次のコードは何を出力するでしょう?

    # Python3
    def func(x):
        x = 1
    
    a = 0
    func(a)
    print(a)
    # ここまで。このprint()は何を出力するか?0?1?
    

    「参照渡し」を知っている人ほど、「1」と答えたくなりそうですが、、

    答えは「0」です。何故か?
    func(a)を呼び出した時点では、axの指すメモリは同じ0ですが、x = 1xの指すメモリは別に確保された1に切り替わります。aおよび、aの指す0には何の変化も無いのです。このあたりは前項の問題とほとんど同じです。

    「変数への代入」ではない場合

    例えばこんなとき。

    # Python3
    a = [0, 1]
    b = a
    a[0] = 2
    print(b)  # 出力は[2,1]
    

    3行目でbaは同じリストを参照するようになります。4行目では、そのリスト(これも参照の列みたいなもの)の最初の成分を、0でなく2のメモリを参照するように切り替えます。全体として見ればabが同じリストを参照していることに変わりはないので、この変更はbにも反映されています。

    これは、前項の関数引数で若干の問題を引き起こします。
    次のコード、前項で見たものにそっくりですが、、、

    # Python3
    def func2(x):
        x[0] = 1
    
    a = [0]
    func2(a)
    print(a[0])
    

    前項とは違い、この出力は「1です。
    関数が呼び出されるとaxの指すリストは一貫して同じで、その一部が書き換えられるからです。これはリストでなくnumpy配列の場合もほぼ同じです。

    まとめ

    以上見てきたように、Pythonの変数は全て参照、関数引数は全て参照渡しです。これらが組み合わさると、結果的に全てを値渡しにした場合(C/C++)と殆ど違いが見えなくなり、あまり意識することなくプログラムを書けるようになっています。

    しかし、リストや配列(や、もっと複雑なクラスオブジェクトなど)のように部分的に書き換え可能なものを扱う場合などには、この違いはかなり重要になってきます。怪しいと思ったら、各変数が何を参照しているか、互いに同じか違うかなどを、その都度考えてみてください。

    ひとまず今回はここまで。おしまい。 ※画像は公式のものです。