2
3

More than 1 year has passed since last update.

【Python】プログラミング初学者のため、変数の話

Last updated at Posted at 2021-10-24

はじめに

プログラミング初学者に向けた記事です。
この記事は、僕が今までにプログラミング学習やプログラムを書いた際に起きた、躓きや挫折を掘り起こしながら、僕自身の理解を深めるための記事でもあります。

ちなみに、僕はプログラムを自由に書けると感じる様になるまでに2年以上は掛かりました(汗)。
そんな僕が、初学者の皆さんに言えることは1つ
エラーの数だけ強くなる
です。

なので、皆さんどんどんエラーを出していきましょう(違)!

変数ってなんぞや

プログラム学習する上で、変数を最初に学習した人が一番多いじゃないかなと思います。少なくとも僕は変数を最初に学習しました。
なんとなく学習しただけだと、変数には値が入っていて、その値を出力したり、上書き出来たりするぐらいの理解になっていると思います。

表面上の理解だけなら、それでも十分ですが少し学習を進めていくと下のソースの実行結果に驚くかもしれません。

python3
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つ宣言・代入します。

c
#include<stdio.h>

int main(){
  char character = 'a'; // 文字型
  int number = 1; // 整数型

  return 0;
}

変数characterには文字のaを代入し、変数numberには数字の1を代入しました。
変数にはそれぞれメモリアドレス(メモリ空間の住所みたいなの)が用意されており、そのメモリアドレス上に値が保存されています。

C言語では変数の割り当てられた、メモリアドレスを出力することができます。
2つの変数のメモリアドレスはこんな感じ

c
#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

メモリアドレスは実行される際に割り当てられるので、実行するたびに変数に割り当てられるメモリアドレスは違います。
なので、出力されるメモリアドレスは毎回違います。
変数に割り当てられたメモリアドレスの確認できたところで、メモリにどのように保存されているのか見ていきましょう。

メモリに保存されたデータ

mojikyo45_640-2.gif

上の画像は簡易のメモリ空間です。左の数字がメモリアドレスで右がメモリの実体(データが入る場所)になっています。
例えば、変数characterのメモリアドレスが 0x00 で、変数numberのメモリアドレスが 0x02 の場合以下の様になります。
mojikyo45_640-2.gif

緑色に塗られて部分が変数によってメモリを確保された場所です。
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言語の記述で他の変数が確保したメモリアドレスを参照することが出来ます。

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は同じメモリアドレスを参照しています。なので、どちらか一方でも参照先の値を操作した場合、もう一方の参照先も同じ値が出力されるはずです?される?されろ。

c
#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のソースをもう一度見てみましょう。

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でも参照先のメモリアドレスを知るための組み込み関数が用意されています。

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の挙動が分かったところで次の例を見てみましょう。

python
def add_num(num):
    num += 1

var = 1

print(var)
add_num(var)
print(var) 
実行結果
1
1

はい、訳分からなくなりました。

どうして仮引数numの値が変わったのに変数varの値は変わらないのでしょうか?
参照しているメモリアドレスを覗いてみましょう。

python
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型とか普段、滅茶苦茶値変更していませんか?

どういうことなのって感じですよね。
実際の挙動を確認してみましょう。

python
var = 10
var += 5 

print(var)
実行結果
15

普通に出来ますよね、じゃあ何が出来ないのでしょうか?
次は値は操作する前と後のメモリアドレスを見てみましょう。

python
var = 10
print(f'{id(var)=}')
var += 5
print(f'{id(var)=}')
実行結果
id(var)=9788896
id(var)=9789056

わぁーお
値を操作した前と後でメモリアドレスが変わってますね!
イミュータブルな型は値を変更するのではなく、メモリの参照先を変更しています。

逆にミュータブルな型は値を変更できるので参照先を変えるのではなく、メモリアドレス先の値を操作しているため、変数自体がさしているメモリアドレスは値を操作しても変わりません。

また、変数は代入する時はメモリアドレスを共有します。

python
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)をやらなければいけない言語の学習をおすすめします。

基礎があれば応用もできますからね。

2
3
18

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
2
3