はじめに
プログラミング初学者に向けた記事です。
この記事は、僕が今までにプログラミング学習やプログラムを書いた際に起きた、躓きや挫折を掘り起こしながら、僕自身の理解を深めるための記事でもあります。
ちなみに、僕はプログラムを自由に書けると感じる様になるまでに2年以上は掛かりました(汗)。
そんな僕が、初学者の皆さんに言えることは1つ
エラーの数だけ強くなる
です。
なので、皆さんどんどんエラーを出していきましょう(違)!
変数ってなんぞや
プログラム学習する上で、変数を最初に学習した人が一番多いじゃないかなと思います。少なくとも僕は変数を最初に学習しました。
なんとなく学習しただけだと、変数には値が入っていて、その値を出力したり、上書き出来たりするぐらいの理解になっていると思います。
表面上の理解だけなら、それでも十分ですが少し学習を進めていくと下のソースの実行結果に驚くかもしれません。
def append_element(l):
l.append(3)
var = [1, 2]
print(var) # [1, 2]
append_element(var)
print(var) # [1, 2, 3]
[1, 2]
[1, 2, 3]
関数append_element
は、引数にリストを受け取り、そのリストに要素を追加するだけの関数です。返り値などは何もありません。その関数の引数に、リスト型のデータを代入した変数var
を引数に呼び出してみます。
呼び出しの前後で変数var
を出力してみると、値が変わっていることに気付くと思います。
おやぁ~?
変数var
に対しては、代入を行っていないのにどうして値が変わってしまったのでしょうか?
そもそも変数とは何なのか、どうやって値を保存しているのか?
変数の値はどこに?
結論から言うと、メモリ(RAM)に保存されているデータです。変数が宣言された際に、メモリ空間上のデータを参照します。
ここからは、同じプログラミング言語のC言語を使って解説していきます。
C言語はPythonに比べ、低レベルの実装が可能なのでこういった話をするときに便利なんですよね^^
また、ここで扱うC言語は、プログラミング言語全般の話になるので、C言語の構文とかはここでは触れませんので、あしからず!
まず、変数を2つ宣言・代入します。
#include<stdio.h>
int main(){
char character = 'a'; // 文字型
int number = 1; // 整数型
return 0;
}
変数character
には文字のaを代入し、変数number
には数字の1を代入しました。
変数にはそれぞれメモリアドレス(メモリ空間の住所みたいなの)が用意されており、そのメモリアドレス上に値が保存されています。
C言語では変数の割り当てられた、メモリアドレスを出力することができます。
2つの変数のメモリアドレスはこんな感じ
#include<stdio.h>
int main(){
char character = 'a';
int number = 1;
printf("character = %p\n", &character);
printf("number = %p\n", &number);
return 0;
}
character = 0x7ffd20e705e3
number = 0x7ffd20e705e4
メモリアドレスは実行される際に割り当てられるので、実行するたびに変数に割り当てられるメモリアドレスは違います。
なので、出力されるメモリアドレスは毎回違います。
変数に割り当てられたメモリアドレスの確認できたところで、メモリにどのように保存されているのか見ていきましょう。
メモリに保存されたデータ
上の画像は簡易のメモリ空間です。左の数字がメモリアドレスで右がメモリの実体(データが入る場所)になっています。
例えば、変数character
のメモリアドレスが 0x00
で、変数number
のメモリアドレスが 0x02
の場合以下の様になります。
緑色に塗られて部分が変数によってメモリを確保された場所です。
0x00
のaは変数character
に代入されている値、0x02
の1は変数number
に代入されいる値ですね。
よく見ると0x03
から0x05
もメモリが確保されてますね。ここも変数number
が確保しているメモリになります。メモリアドレスは 1byte 毎に振られるので、変数character
は 1byte、変数number
は 4byteと分のメモリを確保したことになります。
確保するメモリサイズが変わるのは、変数に入るデータの型の違いです。
変数character
の型はchar型と呼ばれ、変数number
の型はint型と呼ばれます。char型は、1文字の英数字記号などの1バイト文字を保存するだけに対し、int型は-2147483648 ~ 2147483647からの範囲の数字を保存するので、保存するデータが大きい分、確保するメモリサイズも大きくなってしまうですね。
同じメモリアドレスを参照
C言語の記述で他の変数が確保したメモリアドレスを参照することが出来ます。
#include<stdio.h>
int main(){
int number = 1;
int *p = &number; // 変数numberのメモリアドレスを変数pに代入
printf("number = %p\n", &number);
printf("p = %p\n", p);
return 0;
}
number = 0x7ffef581f9bc
p = 0x7ffef581f9bc
変数numberと変数pは同じメモリアドレスを参照しています。なので、どちらか一方でも参照先の値を操作した場合、もう一方の参照先も同じ値が出力されるはずです?される?されろ。
#include<stdio.h>
int main(){
int number = 1;
int *p = &number;
*p += 10;
printf("number = %d\n", number); // number = 11
printf("p = %d\n", *p); // p = 11
return 0;
}
number = 11
p = 11
されました。
変数number
と変数p
は同一のメモリアドレスを参照しているので、どちらかが参照先の値の操作をすると、メモリアドレス上の値が変更され、どちらから呼び出しても同じ値を出力するということになります。
C言語のソースが出てきて、わけわかめになっている人が多いかもですが、ここまでの内容で変数って、メモリ上に保存されてて、変数はそのアドレスを持っているだなって理解で十分です。
あの時君は若かった
ここでいっちゃん最初に登場したPythonのソースをもう一度見てみましょう。
def append_element(l):
l.append(3)
var = [1, 2]
print(var)
append_element(var)
print(var)
[1, 2]
[1, 2, 3]
なんとなく分かりませんか?なぜ変数var
のリストが変化したか。
仮引数l
と変数var
のメモリアドレスがどこを参照しているか見てみましょう。
Pythonでも参照先のメモリアドレスを知るための組み込み関数が用意されています。
def append_element(l):
l.append(3)
print(f'{id(l)=}')
var = [1, 2]
print(f'{id(var)=}')
print(var)
append_element(var)
print(var)
id(var)=140483659655488
[1, 2]
id(l)=140483659655488
[1, 2, 3]
そうゆうことです。
関数append_element
の仮引数l
と変数var
は同じメモリアドレスを参照しているということになります。はい。
同じメモリアドレスを参照しているから、両方の変数の値が変わったということです。
関数append_element
の挙動が分かったところで次の例を見てみましょう。
def add_num(num):
num += 1
var = 1
print(var)
add_num(var)
print(var)
1
1
はい、訳分からなくなりました。
どうして仮引数num
の値が変わったのに変数var
の値は変わらないのでしょうか?
参照しているメモリアドレスを覗いてみましょう。
def add_num(num):
num += 1
print(f'{id(num)=}')
var = 1
print(f'{id(var)=}')
print(var)
add_num(var)
print(var)
id(var)=9788608
1
id(num)=9788640
1
ありゃりゃ、別々のメモリアドレスが出力されましたね。
どうして最初のリスト型のデータを保存した変数はメモリアドレスを共有したのに、整数型のデータを保存した変数はメモリアドレスを共有しなかったのでしょうか?
Pythonの変数って?
Pythonの変数の型は大きく分けて2つに分類することが出来ます。
それはミュータブル(可変)かイミュータブル(不変)かです。
ここでは代表的な型だけ紹介します。
- ミュータブル
- list
- dict
- イミュータブル
- int
- str
- bool
- tuple
list型・tuple型は、値の変更が可能で他のint型・str型・bool型・tuple型は、値の変更が出来ません。
値を変更することが出来ません。
ん?値を変更することが出来ない?
int型とかstr型とか普段、滅茶苦茶値変更していませんか?
どういうことなのって感じですよね。
実際の挙動を確認してみましょう。
var = 10
var += 5
print(var)
15
普通に出来ますよね、じゃあ何が出来ないのでしょうか?
次は値は操作する前と後のメモリアドレスを見てみましょう。
var = 10
print(f'{id(var)=}')
var += 5
print(f'{id(var)=}')
id(var)=9788896
id(var)=9789056
わぁーお
値を操作した前と後でメモリアドレスが変わってますね!
イミュータブルな型は値を変更するのではなく、メモリの参照先を変更しています。
逆にミュータブルな型は値を変更できるので参照先を変えるのではなく、メモリアドレス先の値を操作しているため、変数自体がさしているメモリアドレスは値を操作しても変わりません。
また、変数は代入する時はメモリアドレスを共有します。
var = 10
var2 = var
print(f'{id(var)=}')
print(f'{id(var2)=}')
id(var)=9788896
id(var2)=9788896
代入以外にも実引数と仮引数も同じメモリアドレスを共有します。
これはミュータブルな変数もイミュータブルな変数も同じです。
なので、関数append_element
は実引数にした変数の値が変わり、関数add_num
は実引数にした値が変わらなかったんですね。
終わりに
今回は、Pythonの変数の挙動を一部紹介しました。タイトルに'Python'と銘打っておきながら、途中C言語の内容が出てきてタイトル詐欺になるところでした(笑)
Pythonは優しい言語で、プログラミング学ぶ言語として非常に人気な言語です。しかし、優しい言語ということは難しいところを覆い隠されてしまっているということなので、プログラミング言語としてではなく、プログラミングとしての基礎を学ぶには少し向いてないんじゃないかなと思います。
なので、プログラミングのスキルアップをしたい方は、Pythonだけではなく、静的型付け言語やC言語のようなメモリ管理(GC)をやらなければいけない言語の学習をおすすめします。
基礎があれば応用もできますからね。
乙