LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その11 Python の代入処理の補足

Last updated at Posted at 2023-09-19

目次と前回の記事

今回の記事の内容

前回までの記事では、数値型文字列型list の 3 つのデータ型に関する代入処理しか説明しませんでした。そのため、他のデータ型の代入処理でどのような処理が行われるかについて、疑問に思っている方も多いのではないかと思います。

そこで、今回の記事では前回までの記事で説明できなかった代入処理に関する補足説明を行います。

今回の記事は、〇×ゲームの実装とは直接関係のない内容です。また、細かい話が多いせいで、かなり長くなっていますので、途中で意味が分からなくなったり、先に進みたくなった方は飛ばして次に行ってもかまわないでしょう。

ただし、今回の記事の内容は、代入処理を理解するために重要な内容ではあるので、時間ができたら戻ってきて読んでみることをお勧めします。

Python の代入処理

Python の代入処理が初心者にとってわかりづらいのは、代入するデータの種類によって、プログラム A のようにデータが 複製 されているように見える場合と、プログラム B のようにデータが 共有 されているように見える場合があるせいではないでしょうか。今回の記事では 何故 そのようなことが起きるのか、そして どのような場合に違いが生じる のかについて説明します。

プログラム 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]

データの複製、共有と変数の関係

Python のプログラムでデータを扱う場合、単にデータをプログラムに記述しただけでは、そのデータを後から利用することは できません。下記のプログラムでは、1 というデータを記述していますが、この 1 というデータは、この 行だけで利用 できるデータです。

1

プログラムに記述したデータを後から 何度も利用 したい場合は、変数にデータを代入する必要があります。変数に代入されたデータは、その内容を変更しない限り、何度でも 利用できます。

ここでいう変数には、list の要素など、変数と同じ性質 を持つものを 含みます

厳密には、他にもファイルやデータベースにデータを保存するなどの方法がありますが、それらの方法を取ったとしても、保存したデータを読み込んで利用する際には、基本的にはそのデータを 変数に代入 する必要があります。

下記のプログラムでは、1 行目で a1 を代入することで、1 というデータを後のプログラムで 何度も利用 しています。なお、下記のプログラムの 2、3 行目で行っている計算には特に意味はないので、実行結果は省略します。

a = 1
print(a)
print(a * 5)

データを複製したり、共有するということは、そのデータを 後から利用 したいということです。二度と利用しないデータであれば、それを複製したり、共有することに意味はないからです。そのため、複製または、共有 して利用したいデータは、変数に代入する必要 があります。

要点をまとめると以下のようになります。

  • プログラムに記述したデータは、変数に代入しない限り、記述された場所で 1 度きり しか利用できない
  • 変数に代入されたデータは、その変数に代入されたデータを変更しない限り、何度でも利用 できる
  • 複製または、共有 して利用したいデータは、変数に代入する必要 がある

厳密には、二度と利用しないデータであっても、データを複製、共有することはできますが、そのような処理を行う意味はありません。そこで、以降の記事では、データを複製、共有する場合は、必ずそれらのデータを 後から利用 できるようにするため、変数に代入する という前提で話を進めます。

データの複製と共有の定義

データの複製と共有という用語をこれまで何度も使用してきましたが、その 2 つの用語の正確な意味については説明していませんでした。そこで、この 2 つの用語の意味を説明し、それぞれの 用語の定義 を行うことにします。

本記事ではこれから「データの複製」、「データの共有」、「疑似的なの複製」などの用語を定義して使用しますが、それらは、筆者が 今回の記事の説明を行うために 考えた独自の定義 です。他の記事や文献 などで用いられる用語の意味とは 異なるかもしれない 点に注意して下さい。

複製の定義

複製を goo 辞書で引くと「もとの物と同じ物を別に作ること」という説明がされています。

この説明の「物」は、プログラムでは「データ」に対応します。これらのことから、本記事では データの複製 を「あるデータを元に、同じ内容のデータを別に作ること」と定義します。

上記の定義から、データの複製を行うと、同じ内容 の「元のデータ」と「複製されたデータ」の 2 つのデータ が存在することになります。また、先ほど説明したように、データの複製を行った後で、この 2 つのデータをプログラムで扱うことができるようにするためには、この 2 つのデータを 別々の変数に代入 する必要があります。

上記をまとめると以下のようになります

  • データの複製とは、「あるデータを元に、同じ内容のデータを別に作ること」である
  • データの複製では、複製元のデータと複製先のデータを 別々の変数に代入 して利用する

共有の定義

共有を goo 辞書で引くと「一つの物を二人以上が共同で持つこと」という説明がされています。

この説明の「二人以上」は、プログラムでは「2 つ以上の変数」に対応します。また、この説明の「持つ」は、「変数に値を代入する」ことに対応します。これらのことから、本記事ではデータの共有を「同一のデータを、2 つ以上の変数から共同で利用できること」のように定義します。ここでいう「同一」とは、複製の定義の「同じ内容の」という意味とは異なり、データを管理するオブジェクトが同一 であるという意味を表す点に注意して下さい。

上記の定義から、データの共有を行った場合は、共有するデータを管理する オブジェクト を、2 つ以上の 複数の変数参照 することになります。

上記をまとめると以下のようになります

  • データの共有とは、「同一のデータを、2 つ以上の変数から共同で利用できること」ことである
  • データの共有では、共有するデータを管理する オブジェクト を、2 つ以上の 複数の変数参照 する

代入の種類

複製と共有の定義が定まったので、次は代入処理について掘り下げていくことにします。

代入処理は、以下の 2 種類に分類することができます。

  • 代入するデータを管理するオブジェクトが、他の変数から参照されて いない 場合
  • 代入するデータを管理するオブジェクトが、他の変数から参照されて いる 場合

他の変数から参照されていないデータの代入

「代入するデータを管理するオブジェクトが、他の変数から 参照されていない」場合の代入処理では、基本的には データの 複製共有行われません

その理由を箇条書きで説明すると以下のようになります。

  • 代入するデータを管理するオブジェクトは、どの変数からも参照されていない
  • 代入処理によって、そのデータを管理するオブジェクトが、変数から参照されるようになる
  • 上記の事から、代入処理によって、代入されたデータを管理するオブジェクトは 1 つの変数のみ から参照されることになる
  • このことは 複製の性質 である「複製元のデータと、複製先のデータを 別々の変数に代入 して利用する」に反する
  • このことは 共有の性質 である、「共有するデータを管理するオブジェクトを、2 つ以上の 複数の変数 が参照する」に反する

従って、「代入するデータを管理するオブジェクトが、他の変数から参照されていない」場合の代入処理では、基本的にはデータの複製や共有のことを 考慮する必要はありません

なお、「基本的」と書いたのは、部分的に共有 が行われる場合があるためです。そのことについては後述します。

次に、「代入するデータを管理するオブジェクトが、他の変数から参照されていない」代入処理が、どのような場合 に行われるかについて説明します。

リテラルのみが記述された式を直接代入する場合

「その 7」の記事で説明したように、リテラル1が記述された文が実行されると、そのリテラルを管理する 新しいオブジェクト が作成されます。従って、下記のプログラムのような、リテラルを直接変数に代入する処理では、新しく作成されたオブジェクトの id を変数に格納するという処理が行われます。新しく作成されたオブジェクトは、作成された時点では どの変数からも参照されていない ので、この場合は データの複製も共有も行われません

なお、下記の処理は前回までの記事で紹介済なので、処理の説明や図示は省略します。

a = 1
a = " "
a = [1, 2, 3]

新しいデータが生じる式を代入する場合

これまでの記事では説明していませんでしたが、Python がデータを管理するオブジェクトを 新しく作成する のは、リテラルが記述された文が実行された場合だけではありません。式の計算で 新しいデータが生じる 場合でも、そのデータを管理するオブジェクトが 新しく作られます

いくつか具体例を挙げて説明します。

a = 1 + 2

上記のプログラムでは、以下のような手順で処理が行われます。なお、次の「式の中に変数が記述されている場合」で類似する代入処理を図示しますので、上記のプログラムで行われる処理の図は省略します。

  1. 12 はリテラルなので、それらを管理する新しいオブジェクトを 2 つ作成する
  2. 作成したオブジェクトからデータを取り出す
  3. 取り出した 12 を使って、1 + 2 = 3 を計算する
  4. 3 は、式を計算した結果によって作られた 新しいデータ なので、3 を管理する新しいオブジェクトを作成する
  5. 作成した 3 を管理するオブジェクトの id を a に格納する

式の計算を行う前に、毎回新しいデータを管理するオブジェクトを作成し、その後でそのオブジェクトからデータを取り出すという処理が回りくどいと思う人がいるかもしれませんが、Python が新しいデータを扱う際は、必ず そのデータを管理するオブジェクトを 作成する ことになっているので、このような処理が行われます。

上記のような手順で計算を行う一つの理由は、全てのデータをオブジェクトで管理することによって、式の中にどのようなデータが記述されていた場合でも、共通する手順 で計算を行うことができるというメリットがあるからです。

先ほどの例と同様に、上記の手順 4 で 新しく作成されたオブジェクト は、作成された時点では どの変数からも参照されていない ので、この場合も データの複製も共有も行われません

何かを計算する式の中に変数が記述されている場合

下記のプログラムの 2 行目のように、何かを計算する式の中に変数が記述されている場合は、変数が参照するオブジェクトは 既に作成済 なので、式の中に記述された 変数に対して 新しいオブジェクトが作成されることは ありません

a = 5
b = a + 1

上記のプログラムの 2 行目では、以下のような手順で処理が行われます。

  1. 1 を管理するオブジェクトが作成される
  2. a が参照するオブジェクトと、上記で作成したオブジェクトからデータを取り出す
  3. 取り出した 51 を使って、5 + 1 = 6 を計算する
  4. 6 は、式を計算した結果によって作られた 新しいデータ なので、6 を管理する新しいオブジェクトを作成する
  5. 作成した 6 を管理するオブジェクトの id を a に格納する

下図は上記の処理を図示したものです。

先ほどの例と同様に、上記の手順 4 で 新しく作成されたオブジェクト は、作成された時点では どの変数からも参照されていない ので、この場合も データの複製も共有も行われません

複合データ型と単一データ型の定義の再掲

この記事では、複合データ型と単一データ型という用語を頻繁に使用します。この 2 つの用語は過去の記事で説明しましたが、どちらの用語の意味本記事が独自に定義 したものなので、その定義と性質を再掲します。

複合データ型の定義と性質

  • 複合データ型とは、Python の 任意のデータ型 を、複数組み合わせて データを表現するデータ型のことである
  • 複合データ型のデータは、複数のオブジェクト が管理するデータから 構成 される
  • 複合データ型を 構成するオブジェクト は、list のインデックスのような 識別子 を用いて 参照 する
  • 複合データ型には、list、tuple、dict などがある

単一データ型の定義と性質

  • 単一データ型とは、データを 1 つのオブジェクトが管理 するデータ型のことである
  • 単一データ型には、数値型、文字列型、論理型などがある

複合データ型のリテラルの中に、変数のみが記述されているものがある場合

list のような、複合データ型のリテラルでは、データの中に 変数のみを記述 することができます。言葉の説明では意味が分からないと思いますので、具体例を示します。

[1, 2, 3] という list のリテラルは、数値型の 123 という 3 つの数値型のリテラルが組み合わされて表現されています。この場合は、変数のみのデータは記述されていません。

一方、下記のプログラムの 2 行目で b に代入する list のリテラルは、数値型の 1 というリテラルと、変数 a が組み合わされて表現されています。

a = 1
b = [1, a]

上記のように、複合データ型のリテラルの中に、変数のみが記述されているものがある場合は、b に代入されたデータの 一部の要素 が、a と同じオブジェクトを 共有 するという、部分的な共有 が行われます。なお、部分的な共有は少々複雑なので後で説明します。しばらくの間は、部分的な共有が行われていない 場合の説明を行います。

なお、下記のプログラムの 2 行目の a + 1 のように、複合データ型のリテラルの中の、何かを計算する式変数が記述 されている場合は、部分的な共有は 行われません。下記のプログラムの a + 1 に対応する list の要素には、その式の計算結果である 2 を管理する新しく作成されたオブジェクトの id が代入されます。

a = 1
b = [1, a + 1]

関数呼び出しを代入する場合

関数については詳しくは今後の記事で説明する予定ですが、関数呼び出しは何らかの処理を行い、その計算結果を返すという処理を行います。関数呼び出しの計算結果は Python のデータなので、計算結果を管理するオブジェクト が存在します。従って、a = id(1) のように、変数に関数呼び出しの計算結果を代入する場合は、計算結果を管理するオブジェクトが、他の変数から参照されているかどうか によって行われる処理が異なります。

関数の計算結果を管理するオブジェクトが、他の変数から参照されているかどうかは、関数が行う処理によって異なる ので、関数のプログラムを見るか、関数の使い方の説明書を見て判断するしかありません。

関数呼び出しを変数に代入する処理は、関数呼び出しの計算結果を表すデータを変数に代入する処理なので、行われる処理は 他の代入処理と変わりはありません。そのため、今回の記事では以降は、関数呼び出しを扱わないことにします。

他の変数から参照されているデータの代入

「代入するデータを管理するオブジェクトが、他の変数から 参照されている」ような代入処理は、a = b のような、変数に他の変数を直接代入 する処理で行われます。

前回までの記事で説明してきたように、このような代入処理を行うと、b が格納するオブジェクトの id が a に代入されます。その結果、ab同一のオブジェクトを参照 するようになるため、「共有するデータを管理するオブジェクトを、2 つ以上の 複数の 変数が参照する」という、データの共有の性質を必ず満たす ことになります。

このことから、以下のことがわかります。

変数に他の変数を直接代入 する処理では、常にデータを共有 する処理が行われる。

それにもかかわらず、前回までの記事で紹介した下記のプログラム A と プログラム B では、2 行目で 同じ b = a という、変数に他の変数を直接代入する 処理を実行していますが、3 行目の処理の結果を見ると、プログラム A では「a に代入された値を b複製 しているように 見える」、プログラム B では「a に代入された値を b共有 しているように見える」という違いが生じています。

このような処理が行われる理由については、前回までの記事で詳しく説明しましたが、この違いについて、いまだに頭が混乱している人が多いのではないでしょうか?次は、この違いが生じる理由と、どのような場合に複製しているように見えるかについて説明します。

プログラム 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]

後で説明する「複合データ型のリテラルの中に、変数のみが記述されている」場合を除けば、代入処理によって、データの共有 が行われるのは、「変数に別の変数を直接代入する」場合 だけ です。そこで、今回の記事では 以後しばらくの間 は、特に説明がない場合は、変数に別の変数を直接代入する処理 のみを扱うことにします。

Python の代入に関する不正確な説明

Python の代入処理に対して、以下のような説明をたまに見かけますが、この説明は実際には 不正確 です。なお、下記のノートは、内容に不正確な部分 があるので、赤色の背景 で表示しています。

変数に別の変数を直接代入した際に、下記のような処理が行われる。

  • イミュータブル なデータを代入すると、データが 複製 される
  • ミュータブル なデータを代入すると、データが 共有 される

このような説明は、不正確 であるにもかかわらず、実際に使われることがある ので、何故このような説明が行われるかについて説明します。

ミュータブルとイミュータブル

上記の説明で出てきた、ミュータブルイミュータブル という用語について説明します。この 2 つの用語は、Python のデータ型の 性質 を表す用語です。

Python では、データ型の種類によって、オブジェクトが管理するデータを「後から書き換えることができない」ものと、「後から書き換えることができる 」ものの 2 つに分類できます。後から書き換えることができないデータ型のことを イミュータブル2 (変更不能)なデータ型、書き換えることができるデータ型のことを ミュータブル3 (変更可能)なデータ型と呼びます。

本記事で扱う予定のデータ型について分類すると以下のようになります。なお、() の中は Python でのデータ型の正式名称です。他にもデータ型はありますが、本記事では扱わないので割愛します。これまでに説明していないデータ型も入っていますが、それらについては必要になった時点で説明します。

  • イミュータブルなデータ型
    • 数値型 (int、float、complex)
    • 文字列型 (str)
    • 論理型 (bool)
    • タプル型(tuple)
  • ミュータブルなデータ型
    • リスト型(list)
    • 辞書型(dict)
    • 集合型(set)

これまで区別する必要がなかったので説明していませんでしたが、Python の数値型には「整数」を表す「int4」、「浮動小数点数(小数点以下を含む数値)」を表す「float5」、「複素数」を表す「complex6」の 3 種類があります。これらについては、区別する必要が出てきた時点で説明します。

用紙で例えると、イミュータブルなデータを書き込んだ用紙は、油性ペンで書きこんだ場合のように 後から書き換えることができません。一方、ミュータブルなデータを書き込んだ用紙は、鉛筆で書きこんた場合のように 後から書き換えることができます

後から書き換えることができないものの事を英語で read only と呼びます。例えば、書き込み不能な CD ROM の ROM は read only memory の略です。

不正確な説明が行われる理由

先程の不正確な説明を再掲します。

変数に別の変数を直接代入した際に、下記のような処理が行われる。

  • イミュータブル なデータを代入すると、データが 複製 される
  • ミュータブル なデータを代入すると、データが 共有 される

先ほど説明したように、変数に別の変数を直接代入する処理は、常に 複数の変数で 同じデータを共有する処理 です。そのため、上記の ミュータブル に関する説明は 正しい説明 です。そこで、以降は不正確な上記の イミュータブル に関する説明に 絞って 解説を行います。

「イミュータブルなデータを代入すると、データが複製される」という説明が行われるのは、おそらく以下のような理由ではないかと思います。

  • 「データそのものが複製される」という処理のほうが、「データを管理するオブジェクトの id が複製される」という実際に行われている処理よりも 直観的で理解しやすい
  • 数値型や文字列型などの、良く使われるイミュータブルなデータを代入すると、実際にデータそのものが複製された場合と 同様の処理 を行うことができるため、実質的に 複製されると考えても間違っていない 場合がある
  • C 言語などのプログラム言語では、数値型のデータを代入すると確かにデータそのものが複製される

例えば、プログラム A では、データが複製されたように見える処理が行われます。

しかし、実際には、「イミュータブルなデータを代入すると、データが 複製 される」という説明が 正しくない場合 があります。そのため、この説明をそのまま鵜呑みにすると 間違ったプログラム を記述してしまう可能性があります。

データの複製の再定義

代入処理で実際にはデータの複製が 行われていない にも関わらず、データの複製が行われるように 見える 理由について理解するためには、「データの複製」によって複製されたデータが どのような性質を持つか について理解する必要があります。

本記事では「データの複製」を以下のように定義しましたが、この定義の「同じ内容のデータを別に作る」の部分が抽象的なので、より具体的な定義に修正することにします。

あるデータを元に、同じ内容のデータを別に作ること

「別に作る」から、複製元のデータを元に、複製先のデータが 新しく作られる ことがわかります。以後はこの性質の事を、「データの新規作成」と表記することにします。また、複製元のデータを「データ A」、複製先のデータを「データ B」と表記することにします。

「同じ内容」から、「データ A」と「データ B」は同じ内容を持つことがわかります。このような性質の事を「同値性」と呼びます。

「別に作る」ということは、「データ A」と「データ B」は、異なる 独立した データであることを意味します。そのため、「データ A」と「データ B」のいずれかを編集しても、もう片方のデータの内容は変化しないことがわかります。以後は、この性質の事を「データの独立性」と表記します。

上記のことから、「データの複製」を以下のように 定義し直す ことにします。

データの複製とは、以下のような性質を持つ処理のことである。

  1. 複製元の「データ A」を元に、複製先の「データ B」を新しく作成する(データの新規作成
  2. データ B は、データ A と 同じ内容 を持つ(同値性
  3. データ A、B のいずれかを編集しても、もう片方のデータの内容は変化しない(データの独立性

これまでに何度も説明してきたように、Python の代入処理では、代入するデータを管理するオブジェクトが 複製されることはありません ので、上記の「データの新規作成」の性質を 満たすことはありません。このことから、「イミュータブルなデータを代入すると、データが複製される」という説明の中で使われている「複製」という 用語の意味 が、上記の「データの複製」の定義とは 異なる意味を持つ ということがわかります。

「イミュータブルなデータを代入すると、データが複製される」という説明の中の「複製」という用語は、「複製」という用語の 一般的な意味 とは 異なる意味 を持つ。

複製の目的

ところで、一般論として、物の複製を行う 目的 は何でしょうか?考えられる目的の一つに、物を複製することで、それぞれの物に対して 個別の編集 を行うことができるようにするというものがあるでしょう。具体例としては ノート が挙げられます。店で売られているノートは、設計図を元に、同じ物が大量に 複製 されたものです。複製された直後 のノートは、どのノートも 全てのページが白紙で その内容は同じ ですが、個別のノートには、そのノートを購入した人が それぞれ別の内容 を書きこんで 編集する ことができます。

他の目的としては、編集を目的としない 複製が考えられます。具体的には、 が挙げられます。本はノートと同様に、同じ物が大量に 複製 されたものですが、一般的には本はノートと違ってその中身を 読むだけ で、本の中身を 編集 して書き換えることは しません

本記事で定義した「データの複製」によって作られたデータは、上記の どちらの目的 でも使用することが できます。例えば、編集を目的としない 複製の例として挙げた本は、実際に鉛筆などを使って文字を書き込んで編集することができます。

疑似的な複製

イミュタブルなデータを管理するオブジェクトは、後からデータを編集することができなません。そこで、その性質に合わせて、「データの複製」の 条件を厳しく した、複製後に編集を行うことができない という性質を持つデータの複製を考えることにします。そのような性質を持つデータの複製は、編集を目的としない 複製の目的で のみ 使用することができます。

複製後に編集を行うことができないデータの複製のことを、今回の記事では 以後は「疑似的な複製」と表記し、これまでの「データの複製」のことを区別して「完全な複製」と表記することにします。

なお、今回の記事では「疑似的な複製」と「完全な複製」を 区別することが重要 なので「完全な複製」という表記を行いますが、この表記は冗長なので、次回以降の記事では「完全な複製」ではなく単に「複製」と記述することにします。

次に、完全な複製と 比較 しながら、疑似的な複製に求められる 性質を考察 し、疑似的な複製の 定義 を定めることにします。その後で、イミュータブルな数値型 のデータの 代入処理 が、疑似的な複製の定義を満たす ことを示します。

編集不可能性

「疑似的な複製」が「完全な複製」と異なるのは、複製元と複製先の いずれのデータも編集できない ということです。以後は、この性質の事を「編集不可能性」と表記します。

「データの新規作成」の削除

疑似的な複製では、完全な複製の「データの新規作成」の性質を 満たす必要はありません。例えば、本に決して 書き込みを行わない のであれば、同じ本を 複製 して一人一人が本を所有して読むことと、1 冊の本をみんなで 共有 して読むことに 実質的な違いはない からです。そのため、疑似的な複製の定義から「データの新規作成」の性質を 削除 することができます。

現実の世界で、1 冊の本をみんなで共有した場合は、本を複製した場合と比べて「同時に複数の人がその本を読むことが困難」、「その本を読むためにはその本がある場所まで行かなければならない」という違いがあります。

一方、プログラムでは、同じデータを複数の変数が共有した場合でも、プログラムに変数名を記述するだけで、共有した値を利用することができるので、そのような違いは 問題にはなりません

ピンとこない人は、皆さんが今見ている本記事のような、ウェブページ を思い浮かべると良いでしょう。ウェブページは、ウェブブラウザのアドレスバーにウェブページの 識別子 である URL を入力して閲覧しますが、その際に、離れた場所 にいる 複数の人同時同じ ウェブページを 共有 して見ることができます。また、ウェブページを見る人が、ウェブページの内容を 編集することができない という「編集不可能性」の性質を持つという点でも、疑似的な複製と似ています。

「データの独立性」の修正

疑似的な複製は データを編集できない ので、「データ A、B のいずれかを編集しても、もう片方のデータの内容は変化しない」という、「データの独立性」の性質は 意味を持ちません。しかし、「データの編集」の 意味を考察 することで、「データの独立性」を 別の表現に修正する ことができます。

「データの編集」は、以下の 2 つに分類 することができます。

  • データの内容の 一部 を編集する
  • データの内容の 全て を編集する

上記のうちの、「内容の全てを編集する」という処理は、「全ての内容を削除し、新しい内容で上書きする」という処理を意味しますが、この処理は「データそのものを 破棄 して、新しいデータで置き換える」という処理で 代替する ことができます。そして、そのような処理は、「編集不可能性」に 矛盾しません

分かりにくいと思いますので、このことをノートで例えます。「ノートの内容を全て消しゴムで消してから、別の内容を書きこむ」ことと、「ノートを捨てて、新しいノートに別の内容を書きこむ」ことは、行っている 処理の内容 は確かに 異なります が、そのノートの 内容を利用する立場 から見ると、実質的な違いはありません

そこで、「データの独立性」を「データ A、B のいずれかを 別のデータに入れ替え ても、もう片方の データの 内容は変化しない」のように修正することにします。

疑似的な複製の定義

上記の事から、疑似的な複製を以下のように定義する事にします。

疑似的な複製とは、複製元を「データ A」、複製先を「データ B」とした場合、以下のような性質を持つ処理のことである。ただし、「データ A」と「データ B」は同一のデータであっても構わないものとする。

  1. データ B は、データ A と 同じ内容 を持つ(同値性
  2. データ A と データ B はいずれも内容を 編集することはできない編集不可能性
  3. データ A、B のいずれかを 別のデータに入れ替え ても、もう片方 のデータの 内容は変化しないデータの独立性

理由についてはこの後で説明しますが、イミュータブルな数値型 のデータが代入された変数を、別の変数に代入するという処理は、疑似的な複製の性質を満たします。これが、「イミュータブルなデータを代入すると、データが複製される」という説明が行われる原因ではないかと思います。

この説明を、より正確な説明に直すと「イミュータブルなデータを代入すると、複製元と複製先のデータの編集を行うことができない疑似的な複製 が行われる」のようになるでしょう。

ただし、そのように修正した説明にはまだ 不正確な部分 がります。なぜなら、イミュータブルなデータの代入処理を行った際に、疑似的な複製の性質が 満たされない 場合があるからです。

そのことについては後で説明することにして、最初にイミュータブルな 数値型 のデータの代入処理が、疑似的な複製の定義を満たす ことを説明します。

イミュータブルな数値型のデータの代入処理

プログラム A の a = 5b = a という処理で、イミュータブルな数値型 のデータが代入された変数を別の変数に代入した場合、下図の右のようにイミュータブルなデータを管理するオブジェクトを、複数の変数で共有して 参照することになります。図からわかるように、5 を管理するオブジェクトは複製ではなく、共有されています。疑似的な複製では、「データの新規作成」の条件は 必要とされない ので、この代入処理で、新しいオブジェクトが複製されていなくても問題はありません。

なお、以降の図では イミュータブル なデータを管理するオブジェクトを、ミュータブルなデータを管理するオブジェクトと 区別 ができるように、下図のようにデータを表す長方形の背景色を 灰色 で塗りつぶして表記することにします。

下図の左はプログラム A の 2 行目の b = a の処理を実行した直後の状況を表しています。図からわかるように、ab は同一のオブジェクトを参照しているので、「データ B は、データ A と 同じ内容を持つ」という「同値性」の性質を満たします。

また、数値型の 5イミュータブルなデータ なので、「データ A と データ B はいずれも内容を編集することはできない」という、「編集不可能性」の条件を満たします。

5 はイミュータブルなデータなので、5 を管理するオブジェクトのデータを変更するという方法で、ab に代入されたデータを変更することはできませんが、ab別のデータを代入 してデータを 丸ごと入れ替える という方法で 変数の値を変更 することは可能です。また、そのような代入処理を行っても、変数の値が変更されるのは、代入処理を行った変数だけ で、もう片方の変数の値は変更されません。このことから、「データ A、B のいずれかを別のデータに入れ替えても、もう片方のデータの内容は変化しない」という、「データの独立性」の性質を満たします。

疑似的な複製の 3 つの条件を満たす ことから、イミュータブルな数値型 のデータが代入された変数を別の変数に代入した場合に、疑似的な複製 が行われることが確認できました。

単一データ型でイミュータブルなデータの代入

上記では、数値型のデータの場合の説明を行いましたが、「単一データ型でイミュータブル」なデータであれば、代入処理によって 疑似的な複製 がおこなわれます。その理由は先ほど説明した数値型のデータと同じで、そのようなデータは「疑似的な複製」の定義の 3 つの 条件を全て満たす からです。先ほどの数値型の場合と ほぼ同じ内容 になるので、具体的な処理の説明と図示は省略します。

そのようなデータ型には、数値型文字列型論理型 などがあります。

文字列型のデータに関する補足

文字列型のデータは、ミュータブルなデータだと 勘違い されることがあるので補足します。

C 言語など、文字列型のデータがミュータブルなデータ型であるプログラム言語が実際にありますが、Python では文字列型のデータは、イミュータブル なデータ型です。

Python では + 演算子や、+= 演算子を使って、文字列を連結することができますが、何れの場合でも、連結された文字列を管理する 新しいオブジェクトが作成 されます。連結する文字列を管理するオブジェクトのデータが 変更されることは無い 点に注意して下さい。

下記のプログラムでは、ab に文字列を代入した後で、+ 演算子で ab を連結した文字列を c に代入していますが、実行結果からわかるように ab の値は変化しません。

a = "abc"
b = "def"
c = a + b
print(a)
print(b)
print(c)

実行結果

abc
def
abcdef

下記のプログラムの 3 行目の a += "def" は、a が参照するオブジェクトのデータに "def" を連結する処理のように 誤解されがち ですが、この処理は a = a + "def" と同じ処理を行います。先ほど説明したように、+ 演算子による文字列の連結は、連結された文字列を管理する 新しいオブジェクトが作成 されます。

そのため、下記の実行結果からわかるように、連結する前の 3 行目の a と連結後の 7 行目の a異なるオブジェクトを参照 します。

また、2 行目で b = a を実行することで、ab は同じデータを管理するオブジェクトを共有しますが、a += "def" を実行すると a だけが新しく作られたオブジェクトを参照するようになるので、5、6 行目で ab を表示すると異なる内容が表示されます。

1  a = "abc"
2  b = a
3  print(id(a))
4  a += "def"
5  print(a)
6  print(b)
7  print(id(a))
行番号のないプログラム
a = "abc"
b = a
print(id(a))
a += "def"
print(a)
print(b)
print(id(a))

実行結果

140731431470144
abcdef
abc
2251167654512

過去の記事のノートで、文字列型のデータは、[] を使って list のように文字列の中の特定の文字を取り出すことができることを説明しました。そのため、文字列型のデータを複合データ型だと思った人がいるかもしれませんが、実際には そうではありません

複合データ型は、任意の Python のデータ型 のデータを 組み合わせて データを表現できるという性質を持ちますが、文字列型のデータは、文字以外のデータを扱うことができません。その理由は、文字列型のデータは、文字列の中の個々の文字を 異なる オブジェクトで 管理していない からです。

複合データ型の代入処理

複合データ型のデータは、イミュータブルなデータ であっても、代入処理によって 疑似的な複製 が行われるとは 限りません。以下の説明では、イミュータブルな複合データ型である tuple の説明を行い、tuple の代入処理によって疑似的な複製が 行われない具体例 を挙げます。その後で、Python の代入処理で、疑似的な複製が行われる条件 について説明します。

tuple の性質

Python には tuple7(タプル)という、複合データ型 で、なおかつ イミュータブル なデータ型があります。tuple は後からその要素の値を変更できないという、イミュータブル な性質を除けば、list とほぼ同様の性質 を持ちます。

tuple は複数のデータをプログラムで扱う際に、そのデータが 後から変更されることがない ことが あらかじめわかっている ような場合などで利用します。また、tuple は dict というデータ型のキーとして利用することができます。dict や tuple の具体的な使用方法については、今後の記事で説明する予定です。

tuple の記述方法

tuple のリテラルは () の中に、list と同様に、その要素を半角の , で区切って記述します。下記のプログラムは tuple の記述例と、その記述例と同じ要素を持つ list の記述例です。下記のプログラムでは、t という変数に、様々な tuple を代入し、そのすぐ下の行で、同じ要素を持つ list を l (半角の小文字のエルです)という変数に代入しています。

tl に代入するデータの違いは、()[] に変更されているだけなので、特に難しい点はないと思います。なお、1 行目のように、1 つも要素を持たない tuple のことを空の tuple と呼びます。

t = ()
l = []
t = (1, 2, 3)
l = [1, 2, 3]
t = ("", "×")
l = ["", "×"]
t = ((" ", " ", " "), (" ", " ", " "), (" ", " ", " "))
l = [[" ", " ", " "], [" ", " ", " "], [" ", " ", " "]]

tuple と list のリテラルの記述方法の違いは、基本的には ()[] だけですが、要素が 1 つしかない tuple の場合 のみ、list の場合と比較して若干記述方法が異なります。

要素が 1 つしかない tuple は、下記のプログラムの 1 行目のように、要素の後ろに必ず半角の , を記述 する必要があります。

t = (1, )
print(t)
t = (1)
print(t)

実行結果

(1,)
1

その理由は、tuple を記述する際に使用する () という記号 が、数式でも使われる からです。例えば、(1 + 2) * 3 という式で使われる () は、() の中に記述された式を優先して計算するという意味を持ちます。そのため、(1) のように記述すると、その () は数式で使われる () と同じ意味だとみなされるため、(1) は数値の 1 だとみなされてしまいます。上記のプログラムの 4 行目を実行すると、実行結果の 2 行目で 1 が表示されるのはそのためです。

要素が 1 つしかない tuple の場合に、要素の後ろに , を記述するのは、上記の数式の場合と 区別ができる ようにするためです。

tuple の要素の参照

tuple は list と同様に、[] の中に インデックスを記述 して 要素を参照 します。下のプログラムは tuple の中の特定の要素を表示しています。また、4 行目のように、list と同様の方法で、2 次元配列を表す tuple の要素を表示することもできます。

t = (1, 2, 3)
print(t[0])
t = ((1, 2), (3, 4))
print(t[1][0])

実行結果

1
3

tuple の要素への代入

tuple は イミュータブル なデータ型なので、その要素に値を代入すると下記のプログラムのように、エラーが発生 します。また、具体例は省略しますが、append を使って後から tuple に要素を追加することもできません。

t = (1, 2, 3)
t[0] = 2

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 2
      1 t = (1, 2, 3)
----> 2 t[0] = 2

TypeError: 'tuple' object does not support item assignment

上記のエラーメッセージは、以下のような意味を持ちます。

  • TypeError
    データ型(Type)に関するエラー
  • 'tuple' object does not support item assignment
    tuple なオブジェクト(object)は、要素(item)に値を割り当てる(代入のこと)(assignment)する処理を提供(support)しない(does not)

tuple に関するよくある勘違い

tuple は イミュータブル なデータなので、下記のように、tuple が代入された変数を、別の tuple で上書きできないと 勘違い されがちです。しかし下記のプログラムは問題なく実行することができます。

t = (1, 2, 3)
t = (4, 5, 6)
print(t)

実行結果

(4, 5, 6)

イミュータブルなデータである tuple が 変更できないのは、その tuple を管理する オブジェクトのデータ です。具体的には、tuple の 要素の値を変更 することが できません。上記のプログラムの 2 行目で行っているのは、tuple の要素を変更しているのではなく、t に全く 別の tuple を代入 して丸ごと 入れ替える という処理なので、問題なく実行できます。

下記のプログラムは、+ 演算子を使って tuple を結合していますが、要素の値を変更できないはずの tuple で結合ができてしまう点がおかしいと思う人がいるかもしれません。このように考える人は、tuple に対する + 演算子の処理を 誤解 しています。

a = (1, 2, 3)
b = (4, 5, 6)
c = a + b
print(a)
print(b)
print(c)

実行結果

(1, 2, 3)
(4, 5, 6)
(1, 2, 3, 4, 5, 6)

2 つの tuple に対して + 演算子で計算を行うと、2 つの tuple を結合したデータを管理する 新しいオブジェクト が作られます。元の 2 つの tuple は全く変化しないため、エラーにはなりません。実際に上記のプログラムの実行結果から、結合に使われた ab の内容が全く変化していないことを確認して下さい。

下記の内容は、細かい話なのでノートにしました。

list では += 演算子は、list に要素を追加するという処理が行われますが、tuple の場合は、イミュータブルなので、要素の追加が 行われることはなく、結合された tuple を管理する 新しいオブジェクトが作られます

下記のプログラムでは、4 行目を実行すると、a = a + (4, 5, 6) と同じ処理が実行されるので、a にはその計算結果である (1, 2, 3, 4, 5, 6) を管理する 新しいオブジェクト の id が格納されます。そのため、3 行目と 7 行目で表示される a に格納されたオブジェクトの id が 異なります

また、2 行目で b = a を実行することで、ab は 同じ tuple を管理するオブジェクトを 共有 しますが、a += (4, 5, 6) を実行すると a だけ が新しく作られたオブジェクトを参照するようになるので、5、6 行目で ab を表示すると 異なる内容が表示 されます。

1  a = (1, 2, 3)
2  b = a
3  print(id(a))
4  a += (4, 5, 6)
5  print(a)
6  print(b)
7  print(id(a))
行番号のないプログラム
a = (1, 2, 3)
b = a
print(id(a))
a += (4, 5, 6)
print(a)
print(b)
print(id(a))

実行結果

2251181322304
(1, 2, 3, 4, 5, 6)
(1, 2, 3)
2251180188832

一方、下記のプログラムでは、4 行目を実行すると a が参照するオブジェクトが管理する list の要素に [4, 5, 6]追加 されるので、a が参照するオブジェクトが 変更されることはありません。そのため、3 行目と 7 行目で表示される a に格納された オブジェクトの id と、5、6 行目で表示される ab の内容が 同じになります

1  a = [1, 2, 3]
2  b = a
3  print(id(a))
4  a += [4, 5, 6]
5  print(a)
6  print(b)
7  print(id(a))
行番号のないプログラム
a = [1, 2, 3]
b = a
print(id(a))
a += [4, 5, 6]
print(a)
print(b)
print(id(a))

実行結果

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

このように、同じ += 演算子 が、tuple と list で 全く異なった処理 が行われる点は、間違いやすい ので注意して下さい。

なお、このノートで紹介した tuple のプログラムが、先ほどの 文字列型のデータ に関する補足で紹介したプログラムと 似ている と思った人がいるかもしれませんが、実際に どちらも同じ種類の処理 を行っているため、実行結果も同様の結果になります。

tuple の代入が疑似的な複製にならない例

tuple の要素には、python の任意のデータ を記述することができます。下記のプログラムは、tuple の要素に ミュータブル な list を記述しています。このように、ミュータブルな値を持つ 要素を 含む tuple が代入された変数を、他の変数に代入した場合は、疑似的な複製には なりません。そのことを示します。

a = ([1, 2], [3, 4])
b = a

a に代入された tuple の 要素 は、tuple がイミュータブルなデータなので、下記のプログラムの 3 行目のように tuple の要素の値を変更しようとするとエラーが発生します。

a = ([1, 2], [3, 4])
b = a
a[0] = [5, 6]

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 3
      1 a = ([1, 2], [3, 4])
      2 b = a
----> 3 a[0] = [5, 6]

TypeError: 'tuple' object does not support item assignment

しかし、a に代入された tuple の要素に 代入されている値ミュータブル な list なので、a要素の要素 は、下記のプログラムの 3 行目のように値を代入して変更できてしまいます。また、3 行目を実行した結果、下記の実行結果が示すように ab の値が同時に変化します。このことから、tuple はイミュータブルなデータで あるにも関わらず([1, 2], [3, 4]) という tuple を変数に代入した場合は、疑似的な複製ではなく、データの共有 が行われます。

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

実行結果

([5, 2], [3, 4])
([5, 2], [3, 4])

下図は、上記のプログラムの 3 行目の処理を図示したものです。上記の説明の意味が良くわからない場合は、下図を見て 3 行目の処理でエラーが発生しない理由と、ab でデータが共有されていることを確認して下さい。

イミュータブルなデータの代入が疑似的な複製になる条件

上記の例で、イミュータブルな tuple のデータが疑似的な複製に ならない理由 は、その tuple を構成するオブジェクトのデータの中に、ミュータブルなデータが混じっている ためです。一つでもミュータブルなデータがあれば、そのデータは編集可能なので、「疑似的な複製」に求められる「編集不可能性」の 条件が満たされなくなります。従って、イミュータブルなデータの代入処理 が、疑似的な複製 になる 条件 は、以下のようになります。

データを 構成 する、全ての オブジェクトが管理する データがイミュータブル な場合、そのデータが代入された変数を、他の変数に代入すると、疑似的な複製が行われる

単一データ型のイミュータブルなデータ は、データを構成するオブジェクトが 1 つしかない ので、この条件を満たします。一方、複合データ型が上記の条件を満たすかどうかを判断するためには、そのデータを構成する全てのオブジェクトのデータが全てイミュータブルであるかどうかを 確認する 必要があります。

例えば、(1, 2, 3) はその条件を満たすので、下記のプログラムのように、このデータが代入された変数を、他の変数に代入すると、疑似的な複製が行われます。

a = (1, 2, 3)
b = a
print(a)
print(b)
a = (4, 5, 6)
print(a)
print(b)

実行結果

(1, 2, 3)
(1, 2, 3)
(4, 5, 6)
(1, 2, 3)

まとめ

ここまでの説明で、下記の説明が不正確な理由を説明しました。

変数に別の変数を直接代入した際に、下記のような処理が行われる。

  • イミュータブル なデータを代入すると、データが 複製 される
  • ミュータブル なデータを代入すると、データが 共有 される

正しくは、以下のように理解して下さい。

変数に別の変数を直接代入した際に、下記のような処理が行われる。

  • ミュータブル であろうと イミュータブル であろうと、データが 共有 される

  • データを構成する、全ての オブジェクトが管理する データがイミュータブル なデータを代入した場合は、一見すると複製が行われている ように見える疑似的な複製 が行われる。疑似的な複製 は、複製元と複製先 のデータに対して 編集を行うことができない という性質を持つ

大事なこと なので繰り返しますが、疑似的な複製 は、あくまで複製が行われているように 見えるだけ で、実際には共有 が行われます。本当の意味での複製は行われない 点に注意して下さい。

「データを構成する、全てのオブジェクトが管理するデータがイミュータブルなデータ」という表現は長いので、以後はそのようなデータを「疑似的な複製が行われるデータ」と表記することにします。

部分的な共有

説明を後回しにしていた、今回の記事の前半の「複合データ型のリテラルの中に、変数のみが記述されているものがある場合」で言及した、「部分的な共有」について説明します。

下記のプログラムの 2 行目では、b という変数に、複合データ型のリテラルの中に、a という 変数のみが記述 されているデータを代入しています。

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

実行結果

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

このような場合は、以下のような 非対称部分的な データの共有が行われます。

  • 代入先の変数 b は、リテラルの内部に記述された変数 a のデータを 全て共有 する
  • リテラルの内部に記述された変数 a は、代入先の変数 b一部を共有 する
  • リテラルの内部に記述された変数 a と、代入先の b[1] の要素が 同じデータを共有 する

部分的な共有の確認

このことは、プログラムの実行結果から確認することができます。

  • b の値ある [3, [1, 2]] には、a の値である [1, 2] の全てのデータが表示されているので、ba のデータを全て共有している
  • a 値である [1, 2] には、b の値である [3, [1, 2]] の一部のデータしか表示されていないので、ab の一部のデータを共有している
  • ab[1] の値は同じ [1, 2] なので、ab[1] は同じデータを共有している

また、上記を図で確認することもできます。下図は、上記のプログラムの実行結果を図示したものです。図の 水色の枠内 のオブジェクトが a の値を構成するオブジェクト を、紫色の枠内 のオブジェクトが、b の値を構成するオブジェクト を表します。図を見て、「ba を全て共有する」、「ab の一部を共有する」、「ab[1] が同じデータを共有する」ことを確認して下さい。

上記のプログラムに 続けて、下記のプログラムの 3 行目を実行することでも、「ba を全て共有する」ことを確認できます。ba のデータを完全に共有しているので、下記のプログラムの 3 行目のように、a の要素の値を変更すると、b の値も変化します。

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

実行結果

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

上記のプログラムに 続けて、下記のプログラムの 4 行目を実行することでも、「ab の一部を共有する」ことを確認できます。ab[0]共有しない ので、下記のプログラムの 4 行目のように、b[0] の要素の値を変更しても、a の値は変化しません。

1  a = [1, 2]
2  b = [3, a]
3  a[0] = 5
4  b[0] = 7
5  print(a)
6  print(b)
行番号のないプログラム
a = [1, 2]
b = [3, a]
a[0] = 5
b[0] = 7
print(a)
print(b)

実行結果

[5, 2]
[7, [5, 2]]

部分的な共有で行われる処理と疑似的な複製

b = [3, a] で行われる処理は、要素が 2 つある list を作成した後で、b[0] = 3b[1] = a のような 代入処理 が行われるとみなすことができます。そのため、a に数値型のように、疑似的な複製が行われるデータ が代入されている場合は、b[1] の値は a の値から 疑似的な複製 が行われたものになります。

下記のプログラムでそのことを確かめることができます。下記のプログラムでは、1 行目で疑似的な複製が行われる数値型のデータを a に代入しています。そのため、2 行目を実行すると、a疑似的な複製b[1] に代入されます。そのため、5 行目のように a に別の値を代入しても、b[1] の値は 変化しません。そのことを、実行結果から確認して下さい。

1  a = 1
2  b = [3, a]
3  print(a)
4  print(b)
5  a = 5
6  print(a)
7  print(b)
行番号のないプログラム
a = 1
b = [3, a]
print(a)
print(b)
a = 5
print(a)
print(b)

実行結果

1
[3, 1]
5
[3, 1]

複雑なリテラルが記述されている場合

複合データ型のリテラルに、下記のプログラムのように複数の変数が記述されていたり、記述されている変数に代入されたリテラルの値の中にも変数が記述されているような 複雑な場合 でも、同様の考え方で、一つ一つの代入処理を順番に考えて いくことで、変数どうしの関係を理解することができます。下記のプログラムの 3、4 行目の処理は、以下のように考えると良いでしょう

  1. a = [1, 2]c = [5, a] から c[5, [1, 2]] が代入される
  2. b = [3, 4]d = [b, c] から d[[3, 4], c] が代入される
  3. c = [5, [1, 2]]d = [[3, 4], c] から d[[3, 4], [5, [1, 2]]] が代入される
a = [1, 2]
b = [3, 4]
c = [5, a]
d = [b, c]
print(d)

実行結果

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

頭の中だけで考えると混乱するかもしれませんので、分からなくなったら図を書くことをお勧めします。

まとめ

部分的な共有についてまとめると以下のようになります。

複合データ型のリテラルの中に、変数のみが記述されているものがあるデータがを変数に代入した場合は、以下のような 非対称部分的データの共有 が行われる。

  • 代入先の変数は、リテラルの内部に記述された変数のデータを 全て共有 する
  • リテラルの内部に記述された変数は、代入先の変数の 一部を共有 する
  • リテラルの内部に記述された変数と、代入先の要素が 同じデータを共有 する

その際に、代入先の要素 = リテラルの内部に記述された変数 のような処理が行われる。そのため、部分的な複製が行われるかどうかは、一般的な代入処理と同じ方法 で判断することができる。

完全な複製

Python の代入処理は、データを管理するオブジェクトを 共有 する処理なので、データの 完全な複製 は、Python の 代入文だけで行うことはできません

Python ではデータの完全な複製を行う方法がいくつか用意されています。list の場合は list を表すデータの後ろに .copy() を記述することで、list を完全に複製したデータを作成することができます。下記のプログラムは、2 行目で a.copy() を記述することで、a の値を完全に複製したデータを b に代入しています。そのため、5 行目で a の 0 番の要素に 5 を代入しても、実行結果からわかるように b の値は変化しません。

1  a = [1, 2, 3]
2  b = a.copy()
3  print(a)
4  print(b)
5  a[0] = 5
6  print(a)
7  print(b)
行番号のないプログラム
a = [1, 2, 3]
b = a.copy()
print(a)
print(b)
a[0] = 5
print(a)
print(b)

実行結果

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

他にもいくつか完全な複製を行う方法がありますが、完全な複製を行う際にはいくつか気をつけなければならない点があります。例えば、先ほど紹介した .copy() では、list の完全な複製を うまく行えない 場合があります。それらについて説明すると長くなるので、完全な複製については完全な複製を行う必要が出てきた時点で説明することにします。

以上が Python の変数と代入処理に関する説明ですが、今回の記事でで説明した処理は複雑で、理解しがたい所が多いと思います。筆者もこの記事を書いている最中に、何度も頭が混乱しました。もしかすると、今も少し混乱していて、この記事で書いた内容が間違っているかもしれないと心配になるくらいです(実際に間違っているかもしれません。その場合は指摘していただけると大変助かります)。特に、頭の中だけで考えると訳が分からなくなりがちなので、分からなくなったら、図を書くこと をお勧めします。

また、今回の記事があまり理解できなくても悲観することはありません。プログラミングの技術が上達すればだんだんとわかるようになると思いますので、今回の記事の意味が理解できるようになったと感じたときにもう一度読み返してみて下さい。

その他の補足説明

今回の記事に関連する、その他の補足説明をいくつか行います。

オブジェクトの使いまわし

以前の記事で、Python では 1" " などのリテラルが記述された式が実行されるたびに、そのデータを管理する 新しいオブジェクトが作成される という説明をしました。

しかし、実際には Python では、同じデータを管理するオブジェクトが 過去に作成 されていた場合、新しいオブジェクトを作成する代わりに、そのオブジェクトが 使いまわされる場合があります。オブジェクトが使いまわされる理由としては、プログラムの中の計算式で 良く使われる、数値型や文字列型のデータが記述された文を実行するたびに 新しいオブジェクトを作る のは 効率が悪い ためです。

この補足説明では、どのような場合にオブジェクトが 使いまわされる可能性 があり、どのような場合に使いまわしが 決して行われない かについて説明します。

下記のプログラムはオブジェクトが使いまわされる例です。なお、下記の実行結果は筆者のコンピューターで下記のプログラムを実行した場合のものです。コンピューターの OS や Python のバージョンなどによっては、使いまわされない場合があるかもしれません。

1 行目と 2 行目で同じ数値型の 1 が 2 回記述されていますが、実行結果からオブジェクトの id が等しいので オブジェクトが使いまわされている ことがわかります。

3 行目では、list の要素に同じ数値型の 2 が代入された 2 つの list を結合したデータを a に代入しています。5、6 行目の実行結果から、a の 2 つの要素が、2 を管理する 同じオブジェクトを参照 しており、ここでもオブジェクトが 使いまわされている ことがわかります。

1  print(id(1))
2  print(id(1))
3  a = [2] + [2]
4  print(a)
5  print(id(a[0])) 
6  print(id(a[1])) 
行番号のないプログラム
print(id(1))
print(id(1))
a = [2] + [2]
print(a)
print(id(a[0])) 
print(id(a[1]))

実行結果

140731432669992
140731432669992
[2, 2]
140731432670024
140731432670024

このような使いまわしが行われる 可能性 があるのは、本記事で説明した、疑似的な複製が行われるデータ限ります。具体的には、「データを構成する、全ての オブジェクトが管理する データがイミュータブル」なデータです。それ以外のデータ では、このようなオブジェクトの使いまわしは 決して行われません

疑似的な複製が行われるデータ を管理するオブジェクトが使いまわされる可能性があるのは、そのようなデータは、疑似的な複製の 編集不可能性 の性質から、編集することができない からです。決して編集することができないデータであれば、そのデータを複製しても、共有して使いまわしても、そのデータを使う側 からみると 実質的な違いはない ため、その後のプログラムの 処理に影響を及ぼさない からです。

なお、疑似的な複製が行われるデータ を管理するオブジェクトが必ず使いまわされるとは限りません。例えば、下記のプログラムは 1 行目と 2 行目で同じ数値型の 1.1 が 2 回記述されていますが、筆者のコンピュータで実行すると、実行結果のようにオブジェクトの id が異なるので、オブジェクトが使いまわされて いない ことがわかります。

print(id(1.1))
print(id(1.1))

実行結果

2251180241456
2251165610640

具体的にどのような場合に使いまわしが行われるかについては調べても良くわかりませんでしたが、その法則を 知る必要はありません。オブジェクトの使いまわしは、プログラムの処理の効率をよくするために Python が自動的に行う 処理です。オブジェクトが使いまわされていても、いなくても、プログラムの処理に 影響は与えない ので、プログラムを記述する際に、オブジェクトの使いまわしについて 意識する必要はありません8。従って、プログラムの 処理を理解する 際に、オブジェクトの 使いまわしは行われないと考えても問題はない でしょう。

上記をまとめると以下のようになります。

リテラルが記述された文を実行する際に、そのリテラルを管理するオブジェクトが使いまわされるかどうかは、以下の条件で決まる。

  • 疑似的な複製が行われる データを管理するオブジェクトは、使いまわされる可能性がある。ただし、この条件が満たされている場合でも使いまわされるとは 限らない
  • それ以外のデータを管理するオブジェクトは、決して使いまわされない

オブジェクトの使いまわしは、プログラムの処理の効率をよくするために Python が自動的に行う ものなので、プログラムで行われる 処理を理解する 上では、オブジェクトの 使いまわしは行われないと考えても良い

変数とオブジェクトの図示

本記事では図を使って変数やオブジェクトの構造を説明してきましたが、自分でこのような図を書くのは大変です。そこで、Python のプログラムを記述すると、そのプログラムを実行した際の 変数とオブジェクトを図示 してくれるウェブページ(英語のページです)を紹介します。

このウェブページは以下の手順で使用します。

  1. ページ内の「Start coding now in Python, JavaScript, C, C++, and Java」にある、「Python」のリンクをクリックする
  2. テキストボックスの下にある「inline primitives, don`t nest objects [default]」のメニューをクリックして、「render all objects on the heap (Python/Java)」を選択する。この操作を行わないと、数値型などの、単一データ型のオブジェクトが図示されません
  3. テキストボックスにプログラムを記述し、「Visualize Execution」のボタンをクリックする

下図は、テキストボックスに前回までの記事で紹介した プログラム B を入力してボタンをクリックした場合の図です。

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

このツールは、この画面でプログラムを 1 行ずつ実行しながら、変数とオブジェクトの関係を図示します。図の記号やボタンは以下の意味を持ちます。なお、上記の図はプログラムを実行する前の状態なので、緑色の矢印は表示されていません。

記号やボタン 意味
赤い矢印 次に実行するプログラムの行
緑の矢印 直前に実行されたプログラムの行
Edit this code プログラムを編集する画面に戻る
<<First プログラムを実行開始する前の状態に戻す
<Prev プログラムの処理を 1 つ前に戻す
Next> 赤い矢印の行を実行する
Last>> プログラムの処理を最後まで行う

上記の図で「Next>」ボタンをクリックすると 1 行目のプログラムが実行され、下図のような画面が表示されます。

図の左側では、直前で実行された 1 行目に緑の矢印が表示され、次に実行する 2 行目に赤い矢印が表示されます。

図の右側には、1 行目を実行した時点での、変数とオブジェクトが図示されます。本記事の図とはオブジェクトや変数の表記方法が若干異なりますが、よく見れば a = [1, 2, 3] が図示されていることがわかると思います。なお、本記事の図と大きく異なる点は、このツールでは、「オブジェクトの id が表示されない」点と、「オブジェクトの上にオブジェクトが管理するデータのデータ型が表示される」点です。

図の右側の上部には、print によって表示される内容が表示されます。1 行目には print が記述されていないので、図のこの部分は空欄になっています。

下図は、「Next>」を複数回クリックして、プログラムを最後まで実行した際の、画面の右に表示される内容を表しています。

別のプログラムを実行したい場合は、左の「Edit this code」をクリックして下さい。

このツールでは、「Next>」をクリックするたびに、プログラムが 1 行ずつ実行され、右に表示される変数とオブジェクトの関係の図がそれに伴い更新されていくので、プログラムの 処理の流れ と、それに伴い 変数とオブジェクト がどのように 変化していくかを把握 するには 便利なツール だと思います。

ただし、プログラムが扱うデータが増えると、どうしても右の図が 大きく複雑 になって、全体を把握しづらく なるという 欠点 があります。そのような場合は、自分で図を描いたほうが良いかもしれません

そのような例を実際に確認してみたい人は、10 行 10 列 の初期化されたゲーム盤を作成する board = [[" "] * 10 for x in range(10)] というプログラムをこのツールで入力して、「Last>>」ボタンをクリックしてみて下さい。

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/9/21 「限定的な複製」を「疑似的な複製」に修正しました
2023/9/20 変数に別の変数を直接代入した際に行われる処理の説明を修正しました
2023/9/20 「内容の同一性」を「同値性」に修正しました
  1. 1"abc"[1, 2, 3] などのように、プログラム内に直接記述されたデータの事です。詳細は「その 7」の記事を参照して下さい

  2. immutable。「変わらない」、「不変の」という意味の英単語

  3. mutable。「変わりやすい」、「変更できる」という意味の英単語

  4. 整数を表す integer の略

  5. 浮動小数点数を表す floating point number から付けられています

  6. 複素数を表す complex number から付けられています

  7. 「組」を表す英単語。ここでいう組とは「二つ以上を取り合わせたひとまとまりのもの」という意味を表す

  8. 意識する必要があるとすれば、本記事のように、オブジェクトの仕組みを説明する 必要がある場合くらいでしょう

0
0
11

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