目次と前回の記事
前回のおさらい
前回の記事では繰り返し処理を使って、初期化されたゲーム盤を作成する方法を紹介しました。
しかし、記事の最後で紹介した下記のプログラムは、(1, 0) のマスを表す要素に "〇"
を代入すると、(0, 0) と (2, 0) を表す要素にも 同時に "〇"
が代入 されてしまうという 重大な欠陥 があります。以後は、このプログラムの事を、欠陥のあるゲーム盤を初期化するプログラム と表記します。
board = [[" "] * 3] * 3
board[1][0] = "〇"
print(board)
実行結果
[['〇', ' ', ' '], ['〇', ' ', ' '], ['〇', ' ', ' ']]
なぜこのような現象が起きるかについて 正しく理解する ことは、Python のプログラミングを行っていく上で 非常に重要 で、避けて通ることはできません。
そこで、今回からしばらくの間の記事は、〇×ゲームの実装を一旦中断し、このような現象が起こる理由について詳しく説明することにします。
今回からしばらくの間の記事は、非常に重要な内容ではありますが、初心者には難しい かもしれません。どうしても意味がわからない場合は、今の段階ではざっと目を通しておいて、先に進んでも良いと思います。
今はわからなくても、Python のプログラミングに慣れてくれば、徐々に意味が分かってくるようになると思いまので、意味が分かるようになったと思った段階で、後から読み直して理解することをお勧めします。
逆に、ある程度プログラミングを学んだ方にとっては既に知っていることかもしれません。そのような場合は復習の意味でざっと目を通し、知らない内容があればその部分だけじっくりと読めば良いでしょう。
一見すると奇妙に見えるプログラム
下記のプログラムで行われている処理の内容をいきなり説明するのは難しいので、先に 類似する現象 が発生する、より 簡単な プログラムを使ってこの現象の説明をすることにします。
board = [[" "] * 3] * 3
プログラミングの初心者にとってわかりづらい例として、以下のようなプログラムの例が良く挙げられます。
下記は、以下のような処理を行うプログラムです。このプログラムを以降は、プログラム A と表記します。
- 変数
a
に5
を代入する - 変数
b
に変数a
を代入する - 変数
a
に2
を代入する - 変数
a
とb
を表示する
a = 5
b = a
a = 2
print(a)
print(b)
実行結果
2
5
ほとんどの人が想像するとおり、b
に a
を代入した後で、a
の値を変更しても、b
の値は変化しません。
一方、下記のプログラムは、以下のような処理を行うプログラムです。このプログラムを以降は、プログラム B と表記します。
- 変数
a
に[1, 2, 3]
という list を代入する - 変数
b
に変数a
を代入する - 変数
a
の 0 番の要素に2
を代入する - 変数
a
とb
を表示する
a = [1, 2, 3]
b = a
a[0] = 2
print(a)
print(b)
プログラム A と B の違い
- a = 5
+ a = [1, 2, 3]
b = a
- a = 2
+ a[0] = 2
print(a)
print(b)
実行結果
[2, 2, 3]
[2, 2, 3]
プログラム B は b
に a
を代入した後で、a
の 0 番の要素に値を代入して変更すると、実行結果から b
の値も 同時に変更 されているように見えます。
プログラム A と プログラム B は、どちらも 同じような代入文 の処理を 3 回行うプログラムですが、実行すると結果が大きく異なっているように 見えます。初心者にとって プログラム B は 奇妙な処理 が行われているように思えるかもしれませんが、実際には 正しい処理 が行われています。
プログラム B で起きるこの現象は、欠陥のあるゲーム盤を初期化するプログラムで作られたゲーム盤の (1, 0) のマスを表す要素に "〇"
を代入すると、複数の要素が 同時に変更 されるように 見える 現象とよく似ています。実際に、どちらも 同じ理由 でそのような現象が起きています。
プログラム B のほうが、欠陥のあるゲーム盤を初期化するプログラムより簡単なプログラムなので、先にプログラム B の説明 を行ってから、欠陥のあるゲーム盤を初期化するプログラムについて説明します。
Python のデータとオブジェクト
プログラム A、B で行われている処理の違いを正しく理解するためには、Python がデータをどのように扱っているかの 仕組み を知る必要があります。
Python では 数値型、文字列型、list などの、様々なデータ型を扱うことができますが、それらは 全てオブジェクト という形式で記録されています。オブジェクトは Python のプログラミングにおける非常に重要な概念ですが、かなり複雑なものです。本記事では、オブジェクトに関しては一度にすべてを説明せずに、必要に応じて少しずつ解説していく予定です。
今回の記事では、数あるオブジェクトの機能のうち、データを管理する という機能に絞って説明します。
データがどのように扱われるかについては、プログラム言語によって異なる 場合があります。本記事は Python に関する説明 なので、他のプログラム言語には当てはまらない可能性がある点に注意して下さい。
オブジェクトは、Python が扱う データを管理 するための 入れ物 のようなもので、Python では 全てのデータ型 が、オブジェクトという 共通の形式 で管理されます。
オブジェクトは、英語では「物」、「物体」を表す object という単語で、Python などのプログラミング言語だけでなく、コンピュータでは 良く使われる 用語です。例えば Microsoft Word では、図形や画像などをオブジェクトと呼び、Word のホームタブの編集グループには「オブジェクトの選択」というメニューがあります。
ただし、同じオブジェクトという用語が使われていますが、その意味は 異なる場合が多い ので注意する必要があります。実際に、Python などのプログラム言語のオブジェクトと、Word のオブジェクトは、意味が全く 異なります。
オブジェクトの id(識別子)
Python では、プログラムが扱う データごと にオブジェクトが 作られる ので、大量に作られたオブジェクトどうしを 識別(区別)する必要があります。そのためオブジェクトには、オブジェクトを 識別 するための 整数 の id(識別子)が付けられます。また、オブジェクトは、オブジェクトの id を使って 参照 されます。id はオブジェクトを識別するためのものなので、すべてのオブジェクトに対して 必ず異なる id が付けられます。
オブジェクト id は Python が 自動的につける ものなので、プログラムを記述する際に、オブジェクトの id の 管理を行う必要はありません。
Python などのプログラム言語では、様々な種類の 識別子が用いられます。例えば、変数名は、変数に代入されたデータを参照するための識別子です。また、list はインデックスという識別子を使って、list の要素を参照します。そのため、識別子に関する 用語 が出てきた場合は、何を 識別 しているかについて意識するようにしないと、誤解が発生する可能性がある点に注意して下さい。
用紙の例え
オブジェクトをプログラミング言語の概念だけで説明するとわかりづらいと思いますので、現実の世界の「用紙」で例えながら説明することにします。ここでいう「用紙」は以下のような性質を持つものだと考えてください。
- 好きな内容 を書くことができる
- 必要に応じて 何枚でも 新しい用紙を用意できる
- 全ての用紙には、異なる識別番号 があらかじめ記入されている
- 識別番号を後から 変更することはできない
- 識別番号を使って、特定の用紙を 探し出す ことができる
- 用紙の大きさは決まっておらず、書き込みたい内容に応じて 好きな大きさ の用紙を用意できる
この例えでは「オブジェクト」と「用紙」は以下の表のように対応します。
オブジェクト | 用紙 |
---|---|
オブジェクトが管理するデータ | 用紙に書かれた内容 |
id | 識別番号 |
新しいオブジェクトの作成 | 新しい用紙を用意する |
ものごとをわかりやすく説明するために、似ているものに例えて説明することが良く行われますが、例えたものと、例えられたものが 全く同じ であるということは ほとんどありません。例え話にはどうしても 正確性が欠けてしまう という欠点があるので、例え話はものごとの 概要を把握 するためのものだと考えて下さい。そのため、ものごとを 正確に 理解するためには、概要を把握した後で 例えに頼らずに 正確な知識を身に付ける必要があります。
「用紙」の例えの場合は、オブジェクトの「データを管理する」という側面だけを考えて例えています。今後説明する予定のオブジェクトの他の性質を「用紙」で例えることはできません。
オブジェクトの id の取得
Python では、id
という組み込み関数を使って、()
の中に記述したデータを管理するオブジェクトの id を 取得する ことができます。2 つのデータを管理するオブジェクトの id を調べて 比較する ことで、2 つのデータを 同じオブジェクトが管理 しているかどうかを 調べる ことができます。
下記のプログラムは、数値型、文字列型、list のデータを管理するオブジェクトの id を表示しています。実行結果から、3 つの異なるデータに対して、そのデータを管理するオブジェクトの id が すべて異っている ことが確認できます。
print(id(1))
print(id("abc"))
print(id([1, 2, 3]))
実行結果(実際に表示される数値は、下記と 異なる場合 があります)
140731432669992
140731431470144
1706072311232
オブジェクトの図示
言葉だけではわかりづらいので、以後はオブジェクトを表す図を使って説明します。下図は、上記のプログラムで記述した 3 つのデータを管理するオブジェクトを、以下のようなルールで図示しています。
- オブジェクトを 薄いオレンジ色の用紙 のような図形で表現する
- オブジェクトの id を 図形の上部 に表示する
- オブジェクトの id を表す数値を 赤色の文字 で表示する
- オブジェクトが管理するデータを図形の中の 白い長方形の中 に表示する
- オブジェクトが管理するデータを 黒色の文字 で表示する
オブジェクトの id は Python が他のオブジェクトと 重複しない ように自動的に割り当てるので、新しく Python が作成したオブジェクトの id が何になるかは プログラムを実行してみなければわかりません。そのため、上記のプログラムを実行した時に表示される id は、プログラムを実行するたびに変わる 可能性があります。
また、実際に付けられる id は上記のプログラムの実行結果を見ればわかるように、かなり大きな数字 になることが多く、それを図でそのまま表示するとかなりの場所が必要になってしまいます。
そこで、図ではオブジェクトの id として 筆者がその場で思いついた 適当な 3 桁の数字 を設定しています。図に表示される id に 深い意味は全くありません ので、図の id の数字に何か意味があるのではないかと 勘違いしないよう にして下さい。
オブジェクトの作成とリテラル
1
、"abc"
、[1, 2, 3]
などのように、プログラム内に直接記述されたデータ の事を、リテラル と呼びます。リテラルは、リテラルが記述されているプログラムの内容を書き替えない限り、常に同じデータ を表すので、プログラムの実行中 に別のデータに 変化することはありません。
一方、a
のような 変数 は、代入文によって プログラムの実行中に内容が変化 します。
Python では、リテラルが記述された文が実行 されるたびに、そのリテラルを管理する 新しいオブジェクト が作成されます。一方、変数は既にその中に データが代入済 なので、変数が記述された文が実行されても、その変数に代入された値に対応する新しいオブジェクトは 作成されません。
リテラルが記述された文を実行する際に、そのリテラルが過去に作成されたオブジェクトが管理するデータと 同じ内容 であったとしても、新しいオブジェクト が作成されます。
実際には、一部例外(後述します)があるようですが、リテラルが記述された文を実行するたびに、必ず新しいオブジェクトが作成される と理解しても 問題はありません。
具体例を挙げて説明します。下記は、1.2
という数値型のデータを 2 回記述 し、そのデータを管理する オブジェクトの id を表示 するプログラムです。下記のプログラムを実行すると、全く同じ 1.2
というデータであるにも関わらず、2 つの異なった 1.2
というデータを管理するオブジェクトが作成されます。そのことは、実行結果で 異なる id が表示されることから確認できます。
print(id(1.2))
print(id(1.2))
実行結果(OS や Python のバージョンなどによっては、同じ id が表示される 可能性があります)
1706069801744
1706069799280
このことを図で表すと下図のようになります。下図は、同じ 1.2
というデータを管理する、異なる id を持つオブジェクトが 2 つ作成 される様子を表します。
同じ数値型のデータを表すオブジェクトが複数作られたとしても、計算を行う際に 問題が起きることはありません。わかりづらいかもしれませんので用紙で例えて説明します。
Python のプログラムで 1.2 + 1.2
という式を記述して実行した場合、1.2
という数値型のデータが 2 回記述されているので、先ほどと同様に、数値型の 1.2
というデータを管理する 異なる 2 つのオブジェクト が作成されます。これを用紙で例えると、2 枚の新しい用紙 を用意し、それぞれに 同じ 1.2
という数字を 書きこむ ことに相当します。
2 枚の用紙は 異なる用紙 ですが、計算は用紙に 書かれている 同じ 1.2
という数字を使って行うため、問題なく計算を行うことができます。大事なのは、用紙そのものではなく、用紙の中に書かれている 内容 であるということです。
このことは、オブジェクトでも同様で、1.2 + 1.2
という計算を行う際に使われるのは、オブジェクトそのものではなく、オブジェクトが 管理する 1.2
という データ なので、1.2
というデータを管理するオブジェクトが複数作られても、問題なく計算を行う ことができるのです。
同じデータに対するオブジェクトの使いまわし
先程、同じデータであっても異なるオブジェクトが作られると説明しましたが、下記のプログラムが示すように、数値型のデータや、文字列型のデータの場合は、同じデータに対して同じオブジェクトが割り当てられることもあるようです。
下記のプログラムの実行結果は、筆者のコンピューターで実行した場合のものです。Python のバージョンや、OS などの違いによって、このプログラムを実行した際に、同じデータに対して異なる id が表示される可能性があります。
print(id(1))
print(id(1))
print(id("abc"))
print(id("abc"))
実行結果
140731432669992
140731432669992
140731431470144
140731431470144
具体的にどのような場合に同じオブジェクトが割り当てられるかの法則については、調べてもよくわかりませんでしたが、このような使いまわしは、使いまわしを行っても問題が発生しない場合 にしか行われないので、その具体的な法則を理解することはあまり重要ではありません1。
オブジェクトが使いまわされる理由としては、プログラムの中の計算式で、1 や 0 などの数値型のデータは大量に使われることが多く、毎回そういった数値データが書かれた文を実行するたびに新しいオブジェクトを作るのが 効率が悪い ためです。
用紙の例で例えると、同じ内容を様々な場所で扱いたい場合に、同じ内容が書かれた用紙を必要になるたびに毎回新しく用意するのではなく、その内容が書かれた 1 枚の用紙を 使いまわして 利用することに相当します。
このようなオブジェクトの使いまわしは、使いまわしを行っても計算結果に影響がない数値型や文字列型のデータで行われることはあるかもしれませんが、使いまわしを行うと間違った計算結果の原因となる list などのデータでは 決して行われません。どのような場合に使いまわしをしても良いかについては、これまでの記事で説明した知識ではうまく説明できないので、後の記事で説明します。
オブジェクトの id は実際には使いまわしがされることがあるかもしれませんが、プログラムでオブジェクトに関する処理を理解する際には、以下のように考えても問題はありません。
プログラムでデータが記述された文が実行されると、データごとに 異なるオブジェクトが作成される。
今回の記事のまとめ
今回の記事の内容をまとめると以下のようになります
- オブジェクトは データを管理 するための 入れ物 のようなものである
- オブジェクトには、オブジェクトごとに 異なる id が付けられている
- リテラルとは、
1
や"abc"
のように、プログラムの中に直接記述されたデータのことである - リテラルが記述 されたプログラムの文が 実行されるたび に、そのデータを 管理する オブジェクトが 作成 される
- 変数が記述 されたプログラムを実行した際に、その変数に代入されたデータに対して新しいオブジェクトが 作成されることはない
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
次回の記事
-
そのような法則がわかったとしても、Python のバージョンや種類(Python そのものにもいくつかの種類があります)、Python を実行するコンピュータの OS などによってその法則が異なる可能性があるからです ↩