0
0

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その9 list の仕組み

Last updated at Posted at 2023-09-19

目次と前回の記事

前回のおさらい

前回の記事では Python の代入文で行われる処理について説明し、最後に プログラム A と プログラム B の 3 行目は、見た目は似ているが 異なる処理 を行っているということを説明しました。

プログラム A

a = 5
b = a
a = 2
print(a)
print(b)

実行結果

2
5

プログラム B

a = [1, 2, 3]
b = a
a[0] = 2
print(a)
print(b)

実行結果

[2, 2, 3]
[2, 3, 3]

今回の記事ではプログラム B の 3 行目で行われる処理について説明し、プログラム B で一見すると奇妙な処理を行われる理由について説明します。

Python の list の仕組み

前回までの図では、list を管理するオブジェクトを下図のように表記していました。

図の list には 3 つの要素があり、それぞれの要素に 123 という 3 つの数値型のデータが代入されています。図ではそれらの数値型のデータが、オブジェクトが管理するデータの中に 直接 記録されているように表記されています。

しかし、実際には list を管理するオブジェクトは、list の要素に代入されたデータを上図のように 直接管理してはいません。その理由の一つは、list の要素が 任意のデータ型代入 できるというものです。実際に、初期化されたゲーム盤のデータは、[[" ", " ", " "], [" ", " ", " "], [" ", " ", " "]] のように、list の要素に、他の list を 入れ子で代入しています。このような複雑なデータを 1 つのオブジェクトで管理することは不可能ではありませんが、扱いづらく非効率 です。

そこで、list の要素は 変数と全く同じ仕組み でデータを扱います。そのため、list の要素は、変数と同様 に「代入文で値を代入できる」、「式の中で記述することで代入された値を利用できる」という性質を持っています。

以前の記事で、『本記事では「複合データ型」という用語を、「Python の 任意のデータ型 を、複数組み合わせて データを表現するデータ型」という意味で定義する』という説明をしました。Python では、list などの 複合データ型 は、データの内部に、list の要素のような、変数と同じ性質を持つもの を使って複数のデータを管理します。

具体的には、Python の list の各要素には、変数と同様に「代入されたデータを管理するオブジェクトの id」が格納されます。下図は、このことを [1, 2, 3] という list を例にして図示したものです。

Python では、すべてのデータ は、オブジェクトで管理される ので、list のような 複合データ型 のデータは、複数のオブジェクトが管理するデータから構成 されます。また、複合データ型を構成するオブジェクトは、list のインデックスのような 識別子を用いて参照 します。

上記で説明したように、list の要素は 変数と同じ性質を持つ ので、下図では list の各 要素を 変数と同じ 直方体で表記 し、list の要素の インデックス直方体の上 に表記します。

Python では、上図のような [1, 2, 3] という複合データ型の list を管理するオブジェクトは、以下のような手順で作成されます。

  1. list の要素として記述されている 123 を管理する 3 つのオブジェクトを新しく作成する
  2. list を管理するオブジェクトを新しく作成し、そのオブジェクトが管理するデータの中に、変数と同じ性質を持つ list の 3 つの要素を作成し、それぞれの要素のインデックスを 0、1、2 とする
  3. 3 つの要素に、それぞれの要素に代入するデータを管理する「オブジェクトの id」を格納する

list を管理するオブジェクトは、list の要素に代入されたデータを 直接格納していない ため、list から特定の インデックスの要素の値 を取り出して利用する際には、以下のような処理が行われます。

  1. list を管理するオブジェクトのデータの中から、インデックスが参照 する要素を探す
  2. 探し出した要素が格納する オブジェクトの id を取り出す
  3. 取り出した オブジェクトの id が 参照 するオブジェクトを探す
  4. 探し出したオブジェクトが管理するデータを取り出す

list の要素に代入されたデータを取り出して利用するためには、以下の表のように異なる 2 つの識別子 を使った参照が行われます。ただし、オブジェクトの id に関する処理は Python が自動的に行う ので、プログラムに記述する必要があるのは、インデックス だけです。

識別子 参照先
list の要素のインデックス list の要素
list の要素が格納するオブジェクトの id 要素の値を管理するオブジェクト

list の性質をまとめると、以下のようになります。

  • list を管理するオブジェクトのデータには、list の要素 が格納される
  • list の要素は、変数と同じ性質を持つ ため、要素に代入された データ直接格納しない
  • 従って list を管理するオブジェクトは、list の 要素に代入されたデータ直接管理しない
  • list の要素は、「インデックス」 という 識別子 を使って 参照 される
  • list の要素に代入された値を管理するオブジェクトは、「インデックス」と「要素が格納する id」という 2 つの識別子 を使って 参照 される

また、複合データ型の性質をまとめると、以下のようになります。

  • list のような 複合データ型 のデータは、複数のオブジェクトが管理するデータから構成 される
  • 複合データ型を構成するオブジェクトは、list のインデックスのような 識別子を用いて参照 する

単一データ型の定義

数値型や文字列型のデータは、1 つのオブジェクトが管理します。そのようなデータを表す Python の用語を調べてみたのですが、見つかりませんでした。そこで、本記事では、数値型や文字列型のデータのように、1 つのオブジェクトが管理 するデータ型のことを、複合データ型に対比して、単一データ型 と表記することにします。

なお、この単一データ型という用語は、筆者が考えた用語 なので、一般的なプログラミング用語ではありません。他の記事や文献では、この用語は使われない 点に注意して下さい。

文字列型のデータは、複数の文字を扱うので、プログラム言語によっては複合データ型に分類される場合があります。本記事では以下のような理由から、Python の 文字列型 のデータを 単一データ型に分類 します。

  • 本記事の定義では 複合データ型 のデータは、複数のオブジェクトが管理するデータ から構成される
  • Python の文字列型 のデータは、1 つのオブジェクトで管理 される

list の代入処理

下図は、プログラム B の 1 行目の a = [1, 2, 3] を実行した場合の図です。その下の、これまでの [1, 2, 3] を表す不正確な図と見比べてみて下さい。

これまでの、[1, 2, 3] を表す 不正確な

図からわかるように、a が格納するのは [1, 2, 3] という list を管理する、150 というオブジェクトの id だけ であり、list の要素である 123 を管理するオブジェクトの id は、a を表す直方体には 一切格納されません

このことは、プログラムで単に a と記述しただけでは、a に代入された list の要素 に対する処理を 記述したことにはならない ということを意味します。

list の要素への代入処理

プログラム B の 3 行目の a[0] = 2 では、a に代入された list の 0 番の要素2 を代入しています。この処理は、以下のような手順で行われます。

  1. 2 というリテラルが記述されているので、新しく 2 を管理するオブジェクトを作成する
  2. a が参照するオブジェクトを探す
  3. そのオブジェクトが管理する list の要素の中から、[] の中に記述された 0 番のインデックスの要素を探す。a[0]この要素 の事を表す
  4. 変数と、list の要素は 同じ性質を持つ ので、a[0] の要素に、手順 1 で作成したオブジェクトの id を格納する

下図は、上記の代入処理を図で表したものです。

図の右側の 150 というオブジェクトの id が入っている直方体が a を、図の右側の 369 というオブジェクトの id が入っている直方体が a[0] を表します。

上記の処理では、以下の 2 つが重要なポイントになります。

  • この処理で a が格納するオブジェクトの id は 変化しない
  • 変化するのは a[0] という 要素 が格納するオブジェクトの id だけである

これは、a[0] = 2 が、a ではなく a[0] に対して代入を行っているからです。Python の代入文で行われる処理を正しく理解している人にとってはあたりまえのようなことかもしれませんが、正しく理解していない人や、いい加減に理解している人は a = 2a[0] = 2混同しがち です。

aa[0] は、どちらも 変数と同じ性質 を持ちますが、上記の図からもわかるように、 異なるものを表している ということを、必ず理解 して下さい。

list は複数のデータを入れることができる 容器 に、list の要素は容器の中に入れた 個別のデータ に例えることができます。そう考えると aa[0]全く異なるものを表している ということが理解できるのではないでしょうか?

そして、この違いを理解していないと、Python で良く使われる list などを利用したプログラムを記述する際に、プログラム B や、board = [[" "] * 3] * 3 のような、意図しない処理 1を行うプログラムを記述してしまう原因になってしまいます。さらに、間違った 理由がわからない せいで、正しいプログラムに 修正することができない という大変困った事態にもなるでしょう。

本記事で 3 回の記事に渡ってかなり詳しくこのことに関して説明しているのは、このことを正しく理解しないと正しいプログラムを作ることができない可能性が高いからです。初心者には難しいかもしれませんが、避けて通るわけにはいかないことなので、頑張って理解して下さい。

上図で、a[0] = 2 の代入が行われた後で、1 を管理する、id が 561 のオブジェクトが、どの変数や list の要素からも 参照されなくなった(赤い矢印で結ばれていない)ことが気になった人はいないでしょうか?このオブジェクトのように、どこからも参照されなくなったオブジェクトは、Python のプログラムから 二度と利用することはできなくなります

Python がオブジェクトを記憶するためには、コンピューターの メモリ が使われます。そのため、利用できないオブジェクトのデータを記憶すると、コンピューターのメモリの無駄遣いになってしまいます。そこで、このようなオブジェクトを Python が定期的に探しだして、自動的にメモリから削除する、ガーベジコレクティング2という仕組みがあります。

なお、ガーベジコレクティングは Python が自動的に行う ものなので、プログラムでその処理を 記述する必要はありません

プログラム B の 3 行目で ab が同時に変化する理由

先程の図では、list の要素に値を代入する処理だけに集中して説明したかったので、プログラム B の 2 行目の b = a によって値が代入された b の存在を表記していませんでした。

下図は、先ほどのプログラム B の 3 行目の a[0] = 2 処理を表す図に、b を加えたものです。図の左側では、プログラム B の 2 行目の b = a を実行した結果、ab が list を管理する 同一の オブジェクトを参照することで、同じデータを 共有 しています。

ab同一の オブジェクトを参照しているので、a[0]b[0] はどちらも図では id が 150 のオブジェクトが管理する「インデックスが 0 の要素」という 同一の要素 を表します。a[0] = 2 という処理を行うと、b[0] = 2 という処理が同時に 2 つ行われるように 見える かもしれませんが、a[0]b[0] は同一の要素を表すので、実際には 1 つの処理 しか行われません。

また、図からわかるように aa[0]異なる オブジェクトを 参照 しています。従って、a[0] = 2 を実行しても、a が参照するオブジェクトが 変化することはありません。従って、プログラム B の 3 行目を実行した後も、ab同一の list を共有 したままです。そのため、下記のプログラム B のように、ab を表示すると、同じ list のデータが表示されます。

上記が、プログラム B が一見すると 3 行目の a[0] = 2 によって、ab が同時に変更 されてしまうという、奇妙な処理が行われてしまうように 見える原因 です。プログラム B が行っている 処理の内容を正しく理解 できれば、プログラム B の ab同一のデータを共有 しているため、同じデータが表示されることは奇妙でもなんでもなく、あたりまえの処理 が行われた結果であることがわかるのではないでしょうか。

プログラム B

a = [1, 2, 3]
b = a
a[0] = 2
print(a)
print(b)

実行結果

[2, 2, 3]
[2, 2, 3]

プログラム B の 2 行目のように、list が代入された変数を他の変数に代入するという処理は、list を複製して代入する処理であると 勘違いされがちですが、実際には 同じ list を複数の変数で 共有 するという処理です。

Python では、list に限らず、変数を他の変数に代入 する処理では、データが 複製されることは無い ことを忘れないで下さい。

プログラム C との比較

前回の記事で紹介した、プログラム B の処理を、プログラム A の処理と同様の処理が行われるように修正した下記のプログラム C と、プログラム B を比較してみることにします。

プログラム C

a = [1, 2, 3]
b = a
a = [4, 5, 6]
print(a)
print(b)
プログラム B からの修正箇所
a = [1, 2, 3]
b = a
- a[0] = 2
+ a = [4, 5, 6]
print(a)
print(b)

実行結果

[4, 5, 6]
[1, 2, 3]

下図は、プログラム C の 3 行目で行われる処理を図示したものです。

3 行目で a = [4, 5, 6] が実行されると、[4, 5, 6] を管理する 新しいオブジェクト が作成され、a がこの新しいオブジェクトを 参照する ようになります。結果として、ab異なる list を管理するオブジェクトを参照するようになり、データの 共有が解除 されるため、ab を表示すると異なる内容が表示されます。

下図は、プログラム B の 2 行目で行われる処理を再掲したものです。上下の図を比べて行われている処理の違いを確認して下さい。

なお、図示はしませんが、下記のように、プログラム C の 4 行目に a[0] = 2 という処理を付け加えて実行すると、実行結果から a[0] の要素の値のみが 2 に変更され、b[0] の要素の値は変化しません。これは、プログラム B と異なり、ab には 異なる list が代入されているためです。

a = [1, 2, 3]
b = a
a = [4, 5, 6]
a[0] = 2
print(a)
print(b)
修正箇所
a = [1, 2, 3]
b = a
a = [4, 5, 6]
+ a[0] = 2
print(a)
print(b)

実行結果

[2, 5, 6]
[1, 2, 3]

かなり長くなりましたが、以上が欠陥のあるゲーム盤の初期化を行うプログラムである、board = [[" " * 3] * 3] で行われる処理を理解するために必要な前提知識の説明です。

プログラム A と B の処理のまとめ

プログラム A と B で行われる処理をまとめると以下のようになります。

  • プログラム A、B の 2 行目で行われる b = a の処理は、いずれも、a に代入されたデータを b共有する処理 である
  • プログラム A の 3 行目では、a に 値を代入することで、abデータの共有を解除 したため、ab が異なる値を持つようになる
  • プログラム B の 3 行目では、a ではなく、a に代入された list の要素を表す a[0] に値を代入しているので、ab のデータの 共有は解除されない。そのため、ab は同じデータを持つ

プログラム A の 2 行目の b = a が、データの複製を行っているように見える原因の一つは、プログラム A の 1 行目で a に代入された 数値型のデータ が、list のように、その 内部のデータの一部だけを変更 するようなことが できない からです。そのため、数値型のデータが代入された変数に対して、プログラム B のような処理を行う事は不可能です。プログラム B のような処理を行うためには、変数に、後から データの一部を変更 することができる、list のようなデータを代入する必要があります。

具体的に、どのようなデータ が代入された変数を別の変数に代入すると、データが複製されるように見えるかについての説明は後の記事で説明します。

変数に list を代入する際の注意点と工夫

プログラム B からわかるように、list が代入された変数を 直接他の変数に代入する という処理を行った場合、同一の list の要素の値複数の変数 から変更することができるようになります。このことを 意識せず にプログラムを記述すると、バグの原因 になってしまうため、変数に list を代入 した場合は、そのことを 意識しながら注意深く プログラムを記述する必要があります。

しかし、プログラムに記述された 変数の名前 を見ただけでは、その中に 具体的にどのようなデータが代入されているかはわかりません。そのため、プログラムに記述された 異なる変数 に、同じ list のデータが代入 されているかどうかを 判断することが容易ではない 場合が良くあります。

例えば、プログラム B の場合、a = [1, 2, 3]b = a という 記述の直後 であれば、ab に同一の list が代入されていることを比較的簡単に理解することができるでしょう。一方、そこから 100 行先のように 離れた場所ab に関するプログラムを記述する際に、ab に同一の list が代入されていることをプログラミングの初心者が判断するのは簡単ではないでしょう。また、他人が書いたプログラム を理解しようとした場合は、さらにそのことを判断することが困難になります。

現実の例で例えると、同じ部屋に A さんと B さんが 同居 している場合のことを考えて下さい。これは、プログラミングの用語で言いかえると、A さんと B さんが同じ部屋を 参照 しているということに相当します。その状況で、A さんが自分の住んでいる部屋の模様替えをすると、B さんは何もしていないのに自分の住んでいる部屋が模様替えされたように見えるでしょう。

もちろん、A さんと B さん本人や、A さんと B さんに 親しい人 にとっては、模様替えが 1 回しか行われていないことは簡単にわかると思いますが、A さんと B さんのことを知ってはいるが 疎遠になっている人 や、A さんと B さんのことを 知らない他人 が、A さんと B さんから別々に部屋の模様替えをしたという話を聞いた場合は、模様替えが 別の部屋で 2 回 行われたと 勘違い する可能性が高いでしょう。

なお、この例えでは、「親しい人」が「記述の直後」、「疎遠な人」が「100 行先のように離れた場所」、「知らない他人」が「他人が書いたプログラム」に対応します。

この問題に対する工夫として、変数に list が代入されているかどうかを 区別できるよう にプログラミングを行うという方法があります。具体的には、list が一般的に 複数 のデータを扱うするために使われることを考慮し、list を代入する変数の名前に 英語で複数形の名詞 を使います。例えば 3 人のテストの点数(score)を表す list を代入する変数には scores という複数形の名詞を使います。逆に、1 つ のデータを表す数値型のデータを代入する場合は、score のように 単数形 の名詞を使います3

もちろん、この方法だけでは異なる変数に同一の list が代入されているかどうかを見分けることはできませんが、この工夫によって変数に代入されているデータが list であるかどうかを 常に意識しながら プログラミングを行うことができるようになり、その結果エラーの原因となりやすい list を扱うプログラムの間違い減らすことが期待できます。そこで、本記事でも以後は必要に応じてこの工夫を行うことにします。

他にも、score_list のように、変数名の一部に list を含めたり、分かりづらい所にコメントを記述して説明するなど、様々な工夫が考えられるでしょう。

中に 複数 のものを入れる「表」や「盤」の英単語である table や board のように、単語そのものが複数のものを扱う という 意味を持つ 場合は、1 つの表 を表す list を代入する変数に tables のような複数形は使わないほうが良いでしょう。その理由は、tables という単語は、複数の表 という意味を持つので、複数の表のデータを扱う list を代入する変数の名前にしたほうが良いからです。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

次回の記事

  1. 実際には、プログラム B のような処理が必要になる場合があるので、意図してプログラム B のような処理を記述する場合もあります

  2. garbage collecting。英語で「ごみ収集」という意味

  3. プログラミングで、変数の名前に日本語を使うのが好まれない理由の一つに、日本語の名詞には複数形がないのでこのような工夫を行うことができないというものが挙げられます

0
0
1

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
0
0