見出し画像

Pythonの変数

  前回の関数の初期化についての記事を書いていて思い出したこのが、Pythonを始めたころこんなコードでつまづいたことです。

a = 1
b = 1

えっ、これで!!!
 このどこに引っかかったかというと。これを以下の様に出力してみるとわかると思います。各変数の値とアドレスを出力してみます。

a = 1
b = 1
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')

これを実行すると

a: 1 : 140173729710320
b: 1 : 140173729710320

「なんで変数aとbのアドレスが同じなんだ?」
PythonやRubyなど最近の言語に慣れている方には普通かもしれませんが、僕は少し違和感を感じました。僕が慣れ親しんだC言語でかいてみて、実行してみます。

#include <stdio.h>

void main(){
    int a = 1;
    int b = 1;
    printf("a: %d : %p\n", a, &a);
    printf("b: %d : %p\n", b, &b);
}

実行結果

a: 1 : 0x7ffcce976040
b: 1 : 0x7ffcce976044

変数aとbはそれぞれ異なるアドレスになっています。

続いてC言語でaをインクリメントしてみます。先ほどのコードに追加してみます。

#include <stdio.h>

void main(){
    int a = 1;
    int b = 1;
    printf("a: %d : %p\n", a, &a);
    printf("b: %d : %p\n", b, &b);
    printf("a++\n");
    a++;
    printf("a: %d : %p\n", a, &a);
    printf("b: %d : %p\n", b, &b);
}

実行結果

a: 1 : 0x7ffcce976040
b: 1 : 0x7ffcce976044
a++
a: 2 : 0x7ffcce976040
b: 1 : 0x7ffcce976044

 aをインクリメントすると値は2に変わりますが、アドレスは変わりません。まさに変数はオブジェクトを入れる箱のような入れ物でアドレスはその箱の置き場所、住所と例えられそうです。C言語の書籍にも変数や配列は箱で、ポインターはアドレス(アドレスを指し示す変数と言うのが正しいかもしれません)、箱の番地とか住所なんて比喩されることがあると思います。

しかしPythonでid()関数でアドレスを取り出してみるとどうも動きが違うのです。同じアドレスを指し示します。これではaに1追加したらbも1増えてしまいそうです。実際にPythonで変数aをインクリメントしてみます。

a = 1
b = 1
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')
print('a++')
a += 1
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')

実行結果

a: 1 : 140173729710320
b: 1 : 140173729710320
a++
a: 2 : 140173729710352
b: 1 : 140173729710320

 あれれ、「変数aに1を足すとアドレス変わっちゃうの?」
想定外のことが起こってしまいました。と言ってもちゃんとa=2、b=1になっているのですが、そのためにaのアドレスが変わってしまっています。どうもPythonでは変数は箱という考えが通用しないようです。

Pythonっておもしろい!


  それではPythonはどのように解釈すれば良いのでしょうか?

  • 1という定数が定義される。

  • aという変数には1という定数のアドレスが割り当てられる。

  • 同じようにbにも同じ定数1のアドレスが割り当てられる。

aをインクリメント(1追加する)したときはどうでしょう?

  • aに1を加えた答え2がメモリーに割り当てられる。

  • aという変数に答えの2のアドレスが割り当てられる。

という流れになるでしょう。
これに変数cを追加して、事前にaをインクリメントしたときの答えを代入しておいたらどうなるんでしょうか?

a = 1
b = 1
C = 2
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')
print(f'c: {c} : {id(c)}')
print('a++')
a += 1
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')
print(f'c: {c} : {id(c)}')

実行結果

a: 1 : 140232587313392
b: 1 : 140232587313392
c2 : 140232587313424
a++
a: 2 : 140232587313424
b: 1 : 140232587313392
c2 : 140232587313424

  変数aをインクリメントしたらaのアドレスはcのアドレスと同じになっています。ちょっと不思議ですね。なんかどんな演算をするのか予測して、その答えを見透かしていたような気もします。そんなことはないですけどね。

 また配列をどうなっているでしょうか。先にC言語で

#include <stdio.h>

void main(){
    int a = 4;
    int b = 5;
    int aa[5] = {1,2,3,4,4};
    printf("a: %d : %p\n", a, &a);
    printf("b: %d : %p\n", b, &b);
    printf("aa[]: --: %p\n", &aa);
    printf("aa[0]: %d : %p\n", aa[0], &aa[0]);
    printf("aa[1]: %d : %p\n", aa[1], &aa[1]);
    printf("aa[2]: %d : %p\n", aa[2], &aa[2]);
    printf("aa[3]: %d : %p\n", aa[3], &aa[3]);
    printf("aa[4]: %d : %p\n", aa[4], &aa[4]);
}

実行結果。ループ処理でコードは短くできますが、ここではベタで

a: 4 : 0x7fff276b3ae8
b: 5 : 0x7fff276b3aec
aa[]: --: 0x7fff276b3af0
aa[0]: 1 : 0x7fff276b3af0
aa[1]: 2 : 0x7fff276b3af4
aa[2]: 3 : 0x7fff276b3af8
aa[3]: 4 : 0x7fff276b3afc
aa[4]: 4 : 0x7fff276b3b00

 変数a、bと配列aaはそれぞれ別々のメモリー空間が割り当てられています。 aaは通常連続して確保されています。それぞれの間隔も配列に入れる変数の型のサイズ(今回は4)で、増えていきます。これによりc言語ではポインターをインクリメントして配列の次の値を取り出したりできます。また配列aaのアドレスも配列の先頭のaa[0]のアドレスが同じになっています。配列はまさに連続した箱の集まりです。ちょうど引き出しのようなイメージですね。

Pythonではどうでしょうか?

a = 4
b = 5
aa = [1,2,3,4,4]
print(f'a: {a} : {id(a)}')
print(f'b: {b} : {id(b)}')
print(f'aa: {aa} : {id(aa)}')
print(f'aa[{0}]: {aa[0]} : {id(aa[0])}')
print(f'aa[{1}]: {aa[1]} : {id(aa[1])}')
print(f'aa[{2}]: {aa[2]} : {id(aa[2])}')
print(f'aa[{3}]: {aa[3]} : {id(aa[3])}')
print(f'aa[{4}]: {aa[4]} : {id(aa[4])}')

実行結果は

a: 4 : 140197667963216
b: 5 : 140197667963248
aa: [1, 2, 3, 4, 4] : 140197666776768
aa[0]: 1 : 140197667963120
aa[1]: 2 : 140197667963152
aa[2]: 3 : 140197667963184
aa[3]: 4 : 140197667963216
aa[4]: 4 : 140197667963216

 まずaaアドレスとaa[0]のアドレスが異なりますね。そして注目すべきはa[3]とa[4]とaの3つの変数のアドレスが同じと言うことです。これは入っている値がどれも4で同じで、メモリーに割り当てられた整数値”4”のアドレスを参照しているからです。

 通常このようなことを気にすることなくプログラムを組んでいます。ただ僕の場合、プログラムのバグを修正しているときにはこの仕組みがわかっていることは重要だと思っています。世の中では”参照”、”値渡し”、”ミュータブル”、”イミュータブル”などなど言われているのかもしれません。

 C言語の場合プログラマーが変数を扱うときに値を使うのか、ポインターを使うのか自分で決めることができます。関数で変数を渡す場合、それをアドレスでわたすのか、その場合ポインターで渡すか、または参照でアドレスをとるのかを決めます。これらによりある意味効率のよいプログラムを組むことが出きるのですが、これが原因で不具合を生むことも多くあります。
特にポインターでアクセスする場合に気をつけることが多いかもしれません。ポインターとはコンピュータ内のアドレスを差し指していますから、ある程度コンピュータの仕組みがわかっていないと困ったコードになってしまうことがあります。

#include <stdio.h>

void main(){
    int a = 1;
    int b = 2;
    int *p;
    
    p = &a;
    printf("a=%d\n", *p);
    p++;
    printf("a?=%d\n", *p);
}

例えばこんなコード。
変数aとbに値を入れてポインターを用意します。
変数aのアドレスをポインターpにいれて値を取り出す所まではいいのですが、ここであたかも配列の様にpをインクリメントして次の値を取り出します。これがc言語ではコンパイルもできますし、実行時もエラーが発生しないのです。実際に動かすと

a=1
a?=2

 そしてこの場合bの値が取り出されています。これはたまたま偶然bがaの次に割り当てられていたから取り出せただけです。このような偶然で動くようなことが起こりえます。実際に偶然これでプログラムが動いてテストOKになってプログラムがリリースされるとなぜか異常終了することがあるなんてコードになってしまいます(このての問題なら統合テストまでに普通は潰しされるでしょうね)。これが配列や関数の引数などがからみ、複数のプログラマーで開発しているとコミュニケーションが悪いと発生しがちな問題でしょう。c言語のこの様な特性は問題になり、C++になって参照型も扱えるようになったと記憶しています(違うかな?ちなみにc言語でも&をつけて参照は可能だったはずです)。

 Pythonはどうなっているかというと、基本ポインターしかないというイメージです。変数もリストも辞書型も。そして実行中の生まれる値が管理され、同じ値であれば同じアドレスを持っているという特徴があると思います。これによりメモリーエラーの低減、ガベージコレクションを実現していると思います。
 この記事を書いていてすこし調べたらPythonの関数への引数の渡し方で似たようなことを調べている物はありました。僕が見たいくつかのサイトは”参照値渡し”という表現をされていました。たしかに分からなくはないのですが、僕の理解では参照とはポインターへのアクセスの方法のように思います。たしかに&をつけて参照型と言うのですが…。ただ値が管理されいると言うこととセットで考えるとPythonの変数は参照であるというのも分からなくもないと思います。ちょっと僕の考えが古いんですよねwww

 実際にPythonのコードを見たことはないのですが、機会と時間があれば確認してみたいものです。

 最後に追加。配列の場合はすこし異なります。中身が同じ配列でも定義が違うと配列自体のアドレスは異なります。ただし中身の値は同じアドレスが格納されます。また配列を別の変数に代入したときは2つの配列の変数は同じアドレスを指し示します。参考まで、

aa = [1,2,3]
bb = [1,2,3]
print(f'aa: {aa} : {id(aa)}')
print(f'bb: {bb} : {id(bb)}')
print(f'aa[{0}]: {aa[0]} : {id(aa[0])}')
print(f'bb[{0}]: {bb[0]} : {id(bb[0])}')
print(f'aa[{1}]: {aa[1]} : {id(aa[1])}')
print(f'bb[{1}]: {bb[1]} : {id(bb[1])}')
print(f'aa[{2}]: {aa[2]} : {id(aa[2])}')
print(f'bb[{2}]: {bb[2]} : {id(bb[2])}')
cc = aa
dd = aa.copy()
print(f'aa: {aa} : {id(aa)}')
print(f'cc: {cc} : {id(cc)}')
print(f'dd: {dd} : {id(dd)}')

実行結果

aa: [1, 2, 3] : 140701634373312
bb: [1, 2, 3] : 140701634420928
aa[0]: 1 : 140701635559664
bb[0]: 1 : 140701635559664
aa[1]: 2 : 140701635559696
bb[1]: 2 : 140701635559696
aa[2]: 3 : 140701635559728
bb[2]: 3 : 140701635559728
aa: [1, 2, 3] : 140701634373312
cc: [1, 2, 3] : 140701634373312
dd: [1, 2, 3] : 140701635104064

  aaとbbは中身は同じ配列ですが、異なるアドレスです。しかしaaを代入したccはaaと同じアドレスです。当然配列aaをcopyしてつくったddは異なります。

 この記事では一貫して配列という言葉を使っていますが、pythonの場合は配列と言うのは適切ではないでしょう。c言語のような一繋がりのメモリー空間と言う状態にはないと思います。Pythoんではリスト型と言うように、リストというオブジェクトであると言うのが正しい理解でしょう。


まとめ

Pythonの変数はすべて参照


この記事が気に入ったらサポートをしてみませんか?