LoginSignup
0
1

Pythonで〇×ゲームのAIを一から作成する その19 オブジェクト指向プログラミング

Last updated at Posted at 2023-10-15

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤の全てのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

前回までのおさらい

前回の記事では、ゲーム盤の表示を行う、display_board という関数を定義しました。

オブジェクト指向プログラミング

これまでに記述したプログラムでは、以下の変数と関数を定義しました。

  • 変数

    • board:ゲーム盤を表すデータ
  • 関数

    • initialize_board: 初期化されたゲーム盤のデータを返す
    • place_mark: 指定したマスにマークを配置する
    • display_board: ゲーム盤を表示する

現時点では、上記のように変数と関数の 数が少ない ので、混乱することは無いと思いますが、今後〇×ゲームの実装を進めていく際に、新しい変数や関数 をプログラムに 追加 していくことになります。

一般的に、変数や関数などの 名前の数 が数十、数百と 増えていった際 に、それらの 名前の関連性わかりづらくなる という 問題が発生 します。

また、一般的に、プログラムで扱う データが増えた 場合、関数の 仮引数の数も増えるという傾向 があります。例えば、関数呼び出しの際に、実引数数十もの変数を記述 するのは大変だと思いませんか?

このような問題は、オブジェクト指向プログラミング によって解決することが出来る場合があります。

今回の記事を読んだだけでは、オブジェクト指向プログラミングの仕組みや便利さが良くわからないと思う人が多いかもしれません。今はわからなくても、オブジェクト指向プログラミングを行っていくにつれ、だんだんとその仕組みや便利さが実感できるようになると思います。

また、オブジェクト指向プログラミングには、他にも様々な利点があります。それらについて一度説明すると混乱すると思いますので、利点が実感できるような例が出てきた時点で説明します。

オブジェクト指向プログラミングとは何か

IT 用語辞典では、「オブジェクト指向プログラミング」は、以下のように説明されています。

『関連するデータの集合体と、それを操作する手続きを「オブジェクト」(object)と呼ばれるひとまとまりの単位として一体化し、オブジェクトの組み合わせとしてプログラムを記述する手法』

その 7」の記事では、Python の オブジェクトの機能 のうち、「データを管理する」という機能について説明しました。また、list のような 複合データ型 によって、オブジェクト複数のデータをまとめて管理 する例について説明しました。オブジェクトが 複合データ型を扱うことが出来る ということが、上記の説明の「関連するデータの集合体」に対応します。

オブジェクトには、データを管理する以外の機能として、オブジェクトが 管理するデータに対する処理 を行う、 関数持つことが出来る という機能があります。オブジェクトが 関数の機能を持つことが出来る ということが、上記の説明の「それを操作する手続き」に対応します。

関数のように、複数の文をひとまとめにし、呼び出すことが出来るようにしたもののことを、プログラミング用語では 手続き と呼びます。

オブジェクトを利用することで、関連する複数のデータ と、それらのデータに対する処理を行う 複数の関数ひとまとまりの単位 として扱うことが出来るようになります。そのような機能を持つ オブジェクトを組み合わせて プログラムを記述する手法のことを「オブジェクト指向プログラミング」と呼びます。

これまで何度も利用してきた list は、複数のデータひとまとまりの単位 として扱うことができる オブジェクト です。また、list には、list が管理するデータに対して処理を行う 関数がいくつか定義 されており、list の要素にデータを追加する appendlist が持つ関数 の一つです。

list だけでなく、整数型など、Python が扱うデータオブジェクトによって管理 されており、それらのオブジェクトも 関数を持ちます。例えば、文字列型のデータを管理するオブジェクトは、下記のプログラムのように、 upper という名前の、文字列を大文字に変換した値を返す 関数を持ちます

print("abc".upper())

実行結果

ABC

このように、Python の データオブジェクトで管理 されているので、Python でプログラムを記述すること自体 が、オブジェクトの組み合わせとしてプログラムを記述する オブジェクト指向プログラミング と言っても 間違いではありません。ただし、一般的 には、あらかじめ用意されている オブジェクト だけを利用する場合 は、オブジェクト指向プログラミングとは 言わない のではないかと思います。

Python では、list のような、複数のデータをまとめて扱い、それらに対してする処理を行う関数を持つ オブジェクトを自作する ことが出来ます。具体的には、任意のデータ構造 を持ち、そのデータを 処理する関数 を持つ データ型を自作する ことが出来ます。一般的には、オブジェクト指向プログラミングは、自作した オブジェクトを 組み合わせてプログラムを記述 することを指します。

現在では、C++、JavaScript など、多くのプログラム言語でオブジェクト指向プログラミングを行えるようになっていますが、それぞれの言語で、オブジェクトの仕組み用語 などが 細かい点で異なる 場合があります。本記事では Python のオブジェクト について説明しますが、他の言語を学ぶ際には、オブジェクトの使い方や用語の意味が異なる場合がある点に 注意して下さい

属性とメソッド

Python のオブジェクトに関連する 重要な用語 に、属性メソッド があります。

属性には、インスタンス属性とクラス属性の 2 種類があり、今回の記事では インスタンス属性 について説明します。クラス属性については必要になった時点で説明します。また、インスタンス属性が最も良く使われる属性なので、今後の記事では インスタンス属性 のことを単に 属性表記 することにします。

メソッドには、インスタンスメソッド、クラスメソッド、静的メソッドの 3 種類があり、今回の記事では インスタンスメソッド について説明します。他のメソッドについては必要になった時点で説明します。また、インスタンスメソッドが最も良く使われるメソッドなので、今後の記事では インスタンスメソッド のことを単に メソッド表記 することにします。

属性

Python では、オブジェクトが 管理するデータ の事を 属性(attribute) と呼びます。

Python のオブジェクトの属性は、以下のような性質を持ちます。

  • オブジェクトが管理する 属性 は、属性名 によって 識別 する
  • オブジェクトの属性は、オブジェクト.属性名 のように記述し、属性名の直前には 半角の .(ピリオド)を記述する
  • オブジェクトの属性は、以下のように 変数と同じ性質 を持つ
    • 属性名は 変数名と同じルール 1で名前を付ける
    • 属性名は 名前空間 によって 管理 される(詳細は次回の記事で説明します)
    • = による 代入文 で、データを代入 できる
    • 式の中で記述 することで、属性に 代入された値を利用 できる
    • 属性は、代入されたデータ を管理する オブジェクトを参照 する

実際には、setattr という組み込み関数を使って、属性名に 任意の文字列名前を付ける こともできますが、そのような名前を付けると オブジェクト名.属性名 という方法で属性を記述することが 出来なくなります。本記事ではそのような名前を属性名につけることはありません。

JavaScript などでは、オブジェクトの 属性 の事を プロパティ と呼びますが、属性とプロパティは 異なる意味 を持つ用語です。実際に Python では、オブジェクトの属性とプロパティは異なるものを表します。Python のプロパティの具体的な意味については、必要になった時点で説明します。

メソッド

オブジェクトが持つ 関数 の事を、通常の関数と 区別 して メソッド(method) と呼びます。メソッドは、オブジェクト.メソッド名 のように記述します。関数とメソッドは ほとんど同じ仕組み で処理が行われますが、若干異なる 点があるので、メソッドの定義を管理するオブジェクトの事を関数オブジェクトと 区別 して、メソッドオブジェクト と呼びます。関数とメソッドの 違い の一つは、メソッドの場合は、.メソッド名記述されたオブジェクトが管理するデータに対して 処理が行われるという点です。

例えば、これまでの記事で作成した place_mark という 関数 は、実引数で記述したデータに対して 処理が行われますが、list の append メソッド実引数で記述したデータ だけでなく、.append が記述された list に対しても 処理が行われます。

関数 が、関数名と同じ名前の 変数関数の定義 を表すデータが 代入 されたものであるのと同様に、オブジェクトの メソッド も、オブジェクトの 属性メソッドの定義 を表すデータが 代入 されたものです。属性とメソッドの違い は、変数と関数の違いと同様に、代入されているデータの種類の違いだけ です。属性とメソッドが どちらも オブジェクト.名前 のように記述されるのはそのことが原因です。

まとめ

オブジェクトとメソッドに関するまとめは以下のようになります。

  • オブジェクトが 管理するデータ の事を 属性 と呼ぶ
  • 属性は 属性名 によって識別する
  • 属性は オブジェクト.属性名 のように記述する
  • 属性は 変数と同じ性質 を持つ
  • オブジェクトが持つ 関数 の事を、メソッド と呼ぶ
  • メソッドは、オブジェクト.メソッド名 のように記述する
  • メソッドを呼び出すと、メソッドを 呼び出したオブジェクト対して処理 を行う

組み込み型のクラスとインスタンス

Python では、数値型、文字列型、list、関数、モジュールなど、変数に代入できるデータ は、すべてオブジェクトが管理 します。また、それらのデータを管理するオブジェクトは、クラス(class) と呼ばれる、ひな形 となるオブジェクト から作られます

クラスから作成 された、データを管理するオブジェクトの事を、インスタンス(instance)2 と呼びます。

この説明ではわかりづらいと思いますので、具体例を挙げて説明します。

クラスインスタンス はどちらも オブジェクトの一種 なので、クラスオブジェクトインスタンスオブジェクト のように表記する場合がありますが、表記が長くなるので、本記事では単に クラスインスタンス のように表記します。

組み込み型のクラス

数値型、文字列型、list など、Python で あらかじめ定義 されていて利用できるデータ型のことを 組み込み型 と呼びます。組み込み型のデータは、あらかじめ定義 されている クラスから作られます

Ptyhon の組み込み型の詳細については下記のリンク先を参照して下さい。

組み込み型のクラスには、データ型と同じ名前 が付けられており、それらは組み込み関数の一種として あらかじめ定義 されています。例えば、整数型を表すクラスは int、文字列型を表すクラスは str、list を表すクラスは list です。

実際には 組み込みクラス と表記したほうが正確だと思いますが、下記の Python の公式ドキュメントでは、組み込みクラスを、組み込み関数に含めている ので、上記ではそのように表記しました。

下記のリンク先の組み込み関数の説明の中で、class int のように、先頭に class が表記されているものが組み込みクラスです。

組み込み型のインスタンス

組み込み型のクラスは、通常の関数と同様 に後ろに () を記述して 呼び出す ことができ、その返り値は実引数に記述した 組み込み型のデータ になります。例えば整数型の 1、文字列型の "abc"、list の [1, 2, 3] は、それぞれ下記のプログラムのように記述することができます。

下記のプログラムのように、クラスを呼び出すことで作成 された オブジェクト の事を インスタンス と呼び、クラスからインスタンスを作成することを、インスタンス化 と呼びます。

print(int(1))
print(str("abc"))
print(list([1, 2, 3]))

実行結果

1
abc
[1, 2, 3]

上記のプログラムは、下記のように記述することができるので、初めて上記のようなプログラムを見た人は、整数型の 1 をわざわざ int(1) のように記述する理由がわからないのではないかと思います。

print(1)
print("abc")
print([1, 2, 3])

実行結果

1
abc
[1, 2, 3]

同じ組み込み型のデータを 2 通りの方法で記述できるのは、組み込み型 のデータは、プログラムで 頻繁に記述される ので、本来は int(1) のように記述する必要がある所を、1 のようにクラスの名前と ()省略して記述できる ようにしていることが原因です。従って、1 のように記述しても、int(1) のように記述しても、int という クラスをひな形1 という整数を管理する インスタンスが作成される ことに変わりはありません。

これまでプログラムで記述してきた 1"abc"[1, 2, 3] などの 組み込み型のデータ は、すべて それらの組み込みデータ型を表す クラスから作成 された インスタンス である。

なお、この少し後で説明するような場合を除いて、一般的には、1 のような組み込み型のデータをわざわざ int(1) のように記述する必要はありません。

インスタンスの性質

クラスからは、インスタンスを いくつでも 必要なだけ 作成 することが出来ます。また、作成したインスタンスが管理するデータはそれぞれ 独立している ので、それぞれ のインスタンスが 異なるデータを管理 することが出来ます。

具体例を挙げます。下記のプログラムは、1 行目と 2 行目で ab空の list を代入しています。プログラムで [] を記述した際に、あらかじめ定義 されている list のクラス をひな形に、空の list を管理する 新しい list のインスタンス が作成されます。ab に代入された空の list は 異なるインスタンス なので、3 行目のように、a の list に要素を追加しても、b の値は 変化しません

a = []
b = []
a.append(1)
print(a)
print(b)

実行結果

[1]
[]

厳密にはこの例えは正確ではないのですが、クラス設計図インスタンス を設計図から作成した 製品 のように考えるとクラスとインスタンスの関係が理解しやすいのではないかと思います。例えば、本棚の設計図からいくらでも本棚を作ることが出来ますが、作られた それぞれの本棚 には 異なる本を入れる ことが出来ます。なお、クラスとインスタンスの仕組みについては次回の記事で説明します。

組み込み型の変換

int(1) のような方法で組み込み型のデータを記述する方法は、データ型を変換 する際に使われます。

Python では、一部の例外3を除いて、異なるデータ型に対して + などの演算子で演算を行うとエラーが発生します。例えば、下記は数値型の 1 と文字列型の "2"+ 演算子で演算するプログラムで、実行するとエラーが発生します。

print(1 + "2")

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\019\marubatsu.ipynb セル 5 line 1
----> 1 print(1 + "2")

TypeError: unsupported operand type(s) for +: 'int' and 'str'

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

  • TypeError
    データ型(Type)に関するエラー
  • unsupported operand type(s) for +: 'int' and 'str'
    整数型(int type)と文字列型(str type)に対する(for)+ 演算子(operand)による演算は提供されていない(unsupported)

組み込み型の演算に関する詳細は、下記のリンク先を参照して下さい。

このような場合は、データ型を揃えて 演算を行う必要があります。あるデータ型のデータを 別のデータ型 のデータに 変換 することを データ型の変換 と呼び、Python では、データ型を表す クラスの実引数 にデータを記述して 呼び出す ことで、データ型を変換することが出来ます。

下記のプログラムでは、1 行目の int("2") によって文字列型の "2" を整数型の 2 に変換した結果、1 + 2 が計算されて 3 が表示されます。2 行目の str(1) で整数型の 1 を文字列型の "1" に変換した結果、"1" + "2" が計算されて 12 が表示されます。

print(1 + int("2"))
print(str(1) + "2")

実行結果

3
12

見た目からは区別できませんが、実行結果に表示されている 12 は数値型ではなく、文字列型の "12" を表しています。組み込みデータ型の種類type という、実引数に記述したインスタンスを 作成したクラス を返り値として返す組み込み関数を使って調べることが出来ます。下記のプログラムの実行結果から、1 + int("2") の計算結果は整数型(int)、str(1) + "2" の計算結果は文字列型(str)のクラスから作成されたインスタンスであることを確認することが出来ます。

print(type(1 + int("2")))
print(type(str(1) + "2"))

実行結果

<class 'int'>
<class 'str'>

データ型の変換に関する注意点

データ型の変換がどのように行われるかについてはデータ型を表すクラスの種類によって異なります。例えば、下記のプログラムのように、数値型に変換することが出来ない "a" を整数型に変換しようとするとエラーが発生します。

print(int("a"))

実行結果

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\019\marubatsu.ipynb セル 8 line 1
----> 1 print(int("a"))

ValueError: invalid literal for int() with base 10: 'a'

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

  • ValueError
    値(Value)に関するエラー
  • invalid literal for int() with base 10: 'a'
    "a" は、10 を基数(base)とする(10 進数の事)数値型としては不正(invalid)なリテラル(literal)である

クラス名を呼び出した際にどのような処理が行われるかについて詳しく知りたい方、下記のリンク先を参照して下さい。

クラスとインスタンスのまとめ

ここまでで説明したクラスとインスタンスに関するまとめは以下のようになります。

  • データを管理するオブジェクトは、クラス と呼ばれる ひな形 となるオブジェクトから作られる
  • クラスから作成されたオブジェクトの事を インスタンス と呼ぶ
  • あらかじめ定義 されていて利用できるデータ型のことを 組み込み型 と呼び、組み込み型の ひな形 となる クラスあらかじめ定義 されている
  • 組み込み型 のデータを管理するオブジェクトは、その組み込み型の クラス から作成された インスタンス である
  • インスタンスは いくつでも作る ことが出来、インスタンスが管理するデータは 独立 している

カスタムデータ型のクラスとインスタンス

関数を定義して自作できるのと同じように、オブジェクトが管理する 属性 と、メソッド に関する処理を記述した クラスを定義 して 自作する ことができ、そのようなクラスの事を カスタムクラス4 と呼びます。

カスタムクラスを定義 し、インスタンス化 する事で、任意のデータ構造を管理し、そのデータを処理するメソッドを持つ カスタムデータ型 を利用することが出来るようになります。

例えば、〇×ゲームに関する データ を管理する 属性 と、〇×ゲームに関する 処理 を行う メソッド を持つ カスタムクラス を定義することで、〇×ゲームを表すカスタムデータ型 を利用することが出来ます。

表記が長くなるので、組み込み型のクラスと 区別する必要がない場合 は、以後はカスタムクラスのことを単に クラス と表記します。

クラスの定義

クラスの定義は以下のように記述します。下記ではメソッドを 1 つだけ定義していますが、必要な数だけ、複数のメソッドを定義 する事もできます。また、クラスの定義の中でメソッドの定義以外の記述を行うこともできますが、今回の記事では説明しません。その点については次回の記事で説明します。

class クラス名:
    def メソッド名(self, 仮引数):
        メソッドで行う処理を記述するブロック

クラス名 は、他の名前一目で区別 ができるように一般的に 頭文字が大文字の英語 で記述します。頭文字が大文字でないクラス名をつけることもできますが、避けたほうが良い でしょう。

ただし、あらかじめ定義されている 組み込み型 のクラスの名前は 例外 で、名前の 頭文字は小文字 になっています。

変数、関数、クラスなどの名前に、日本語 を使うことが 好まれない 原因の一つは、クラスの名前 であるかどうかを 一目で見分ける ことが出来るような名前の 工夫が難しい からです。

メソッドの定義など、クラスに関する プログラムの 記述 は、クラスの ブロックの中に記述 します。

メソッドの定義は、通常の 関数の定義とほとんど同じ ですが、最初の仮引数self を記述 する点が異なります。self についてはこの後で詳しく説明します。

クラスのブロックに 処理を何も記述しない 場合は、ブロックの中に pass のみの行 を記述します。

下記は、〇×ゲームのデータとメソッド を管理する Marubatsu という名前の クラスを定義 しています。ただし、現時点 ではこのクラスには何の処理も記述していないので、このクラスは属性やメソッドを 持ちません。この後でプログラムを追加していくことで、Marubatsu クラスを完成させていきます。

class Marubatsu:
    pass

Python では、関数の定義がオブジェクトで管理されるのと同様に、クラスの定義オブジェクトが管理 します。クラスの定義を管理するオブジェクトのことを クラスオブジェクト と呼びます。

インスタンス化

カスタムクラスから インスタンスを作成 する方法は、組み込み型のクラスからインスタンスを作成する場合と同様に、クラス名の後に () を記述して クラスを呼び出す ことで行います。() の中に 実引数を記述 する場合こともできますが、その点については次回の記事で説明します。

カスタムクラスのインスタンス化の場合は、組み込み型で int(1)1 のように記述するような、簡潔なインスタンス化 の記述方法は ありません

なお、インスタンス化関数呼び出し は、見た目 だけでは 区別が困難 なので クラスの名前を大文字 にして 区別できるようにする ことは非常に 重要 です。

インスタンスは、ひな形 となる クラスの定義で記述 された 属性とメソッド利用することができます。先ほどの Marubatsu クラスの定義には、属性もメソッドも 記述されていない ので、Marubatsu() を記述することによって作成されたインスタンスは、属性やメソッドを一つも持ちません。

クラスやインスタンスは、実際にはいくつかの 特殊属性特殊メソッド と呼ばれるものを持ちます。特殊属性と特殊メソッドは、クラスの定義に 記述しなくても、オブジェクトが作られる際に 自動的に オブジェクトが管理する属性やメソッドに 組み込まれる ものです。特殊属性と特殊メソッドについては、この後で説明します。

組み込み型の所で説明したように、インスタンス はクラスから いくつでも作る ことが出来ます。また、それぞれのインスタンスが 管理するデータ独立 しています。下記のプログラムは、Marubatsu クラスのインスタンス2 つ作成 し、それぞれを mb1mb2 という 変数に代入 しています。

なお、本記事では、Marubatsu のインスタンスを代入する 変数の名前 は、maru と batsu の 頭文字 をとって、mb で始まる名前 を付けることにします。

mb1 = Marubatsu()
mb2 = Marubatsu()

属性への値の代入と式の中での利用

Marubatsu クラスの定義には 何も記述されていない ので、Marubatsu クラスをひな形として作成されたインスタンスには、(特殊属性と特殊メソッドを除くと)属性とメソッド は一つも 存在しません

先ほど説明したように、インスタンスの 属性変数と同じ性質 を持ちます。従って、値を代入 することによって 変数が作られる のと同様に、値を代入 することによって、インスタンスに新しい 属性を作る ことが出来ます。また、値が代入された属性 は、変数と同様に 式の中で記述 することで 代入された値を利用 することが出来ます。

下記のプログラムでは、1、2 行目で 異なる Marubatsu のインスタンスが 代入された mb1mb2a という属性にそれぞれ 12 を代入しています。mb1mb2 は同じクラスから作られたインスタンスですが、それぞれは 独立したデータを管理する ので、3、4 行目の実行結果からわかるように、mb1.amb2.a には 異なるデータが代入 されます。

mb1.a = 1
mb2.a = 2
print(mb1.a)
print(mb2.a)

実行結果

1
2

値が代入されていない変数名 を式の中で記述すると NameError という エラーが発生 するのと同様に、下記のプログラムのように、値が代入されていない属性名 を式の中で記述すると、AttributeError という エラーが発生 します。

print(mb1.b)

実行結果

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\019\marubatsu.ipynb セル 12 line 1
----> 1 print(mb1.b)

AttributeError: 'Marubatsu' object has no attribute 'b'

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

  • AttributeError
    オブジェクトの属性(attribute)に関するエラー
  • 'Marubatsu' object has no attribute 'b'
    Marubatsu というオブジェクト(object)は b という属性(attributi)を持たない(has no)

クラスのメソッドの定義

クラスのメソッドの定義は、「クラスのブロック に記述する」、「最初の仮引数self を記述 する」以外の点では、通常の関数と同じ方法 で記述します。

最初の仮引数 self には、その メソッドを呼び出したインスタンスが代入 されます。

具体例を挙げて説明します。下記のプログラムの 1 ~ 3 行目では、Marubatsu のクラスの定義の中で、initialize_board メソッドを定義 しています。従って、5 行目でこの Marubatsu クラスから作成されたインスタンスは、6 行目のように、initialize_board という メソッドを呼び出して利用することが出来ます。6 行目で行われる処理の内容についてはこの後で説明します。

1  class Marubatsu:
2      def initialize_board(self):
3          self.board = [["."] * 3 for y in range(3)]
4
5  mb = Marubatsu()
6  mb.initialize_board()
7  print(mb.board)
行番号のないプログラム
class Marubatsu:
    def initialize_board(self):
        self.board = [["."] * 3 for y in range(3)]

mb = Marubatsu()
mb.initialize_board()
print(mb.board)

実行結果

[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]

インスタンスからのメソッドの呼び出し

インスタンスのメソッドは 関数と同様の方法 で、インスタンス.メソッド名(実引数) を記述することで呼び出すことができます。ただし、通常の関数呼び出しとは以下の点が異なります。なお、実引数が記述された場合の具体例については、この次の place_mark の所で説明します。

  • 呼び出されたメソッドの 仮引数 self に、メソッドを呼び出したインスタンスが代入 される
  • 実引数 が記述されている場合は、self の後 に記述された仮引数に対して 順番に代入 される

上記のプログラムの、6 行目の mb.initialize_board() を実行すると、2、3 行目で定義 された initialize_board というメソッドが呼び出されます。その際に、仮引数 self には、このメソッドを 呼び出したインスタンス が代入されている グローバル変数 mb の値が代入 されます。具体的には、下記のプログラムのような処理が行われると考えると良いでしょう。

self = mb
self.board = [[" "] * 3 for y in range(3)]

従って、mb.initialize_board() を実行すると、mb.board = [[" "] * 3 for y in range(3)]同じ意味を持つ処理 が行われ、mb.board初期化されたゲーム盤が代入 されます。

このように、メソッドのブロックの中 では、メソッドを呼び出した インスタンスが管理するデータに対する処理 を、self を使って記述することが出来ます。従って、インスタンスが管理するデータ を、メソッド呼び出しを行う際に、実引数に記述する必要はありません

仮引数 self は、メソッドを呼び出したオブジェクト 自身(self)を表すデータなので、self という名前が使われますが、これは 慣習 なので、この仮引数の名前を self 以外の名前にすることもできます。ただし、self 以外の名前にするとプログラムの意味が非常にわかりづらくなるので self を別の名前に 変更しない ようにすることを強くお勧めします。

メソッドの initalize_board と関数の initialize_board の違い

上記の initialize_board メソッド と、これまでに定義した下記の initialize_board という 関数 には、もう一つ 異なる点 があります。それは、上記の initialize_board メソッドでは、board という属性 に初期化されたゲーム盤のデータを 代入 しているのに対し、下記の関数の initialize_board では、初期化されたゲーム盤のデータを 返り値として返している 点です。その理由について説明します。

def initialize_board() -> list[list[str]]:
    return [["."] * 3 for y in range(3)]

関数initialize_board は、下記のプログラムのように、初期化されたゲーム盤を 異なる複数の変数代入できるようにする ために、関数の 返り値 に初期化されたゲーム盤のデータを 返していました

board1 = initialize_board()
board2 = initialize_board()

一方、Marubatsu クラスから作成された インスタンス は、それ自体が 1 つの 〇×ゲームに関するデータを管理する オブジェクト です。そのため、Marubatsu クラスから作成された インスタンスの中 には、ゲーム盤のデータを代入する属性は 1 つだけあれば十分 です。複数のゲーム盤のデータ扱いたい場合 は、下記のプログラムのように、Marubatsu から 複数のインスタンスを作成しそれぞれのインスタンスに対して initialize_board メソッドを呼び出すことで実現できます。

1  mb1 = Marubatsu()
2  mb1.initialize_board()
3  print(mb1.board)
4  mb2 = Marubatsu()
5  mb2.initialize_board()
6  print(mb2.board)
行番号のないプログラム
mb1 = Marubatsu()
mb1.initialize_board()
print(mb1.board)
mb2 = Marubatsu()
mb2.initialize_board()
print(mb2.board)

実行結果

[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]

上記のプログラムの 3、6 行目の実行結果から、mb1mb2 に代入された インスタンスの board 属性 に、初期化されたゲーム盤のデータが 代入 されていることを確認することが出来ます。なお、この 2 つの board本当に独立したデータであるかどうか については、この後で place_markdisplay_board メソッドを定義してから 確認する ことにします。

メソッドの定義とメソッドの呼び出しのまとめ

メソッドの定義とメソッドの呼び出しに関するまとめは以下のようになります。

  • メソッドの定義は、クラスの定義のブロックの中に、関数と同様の方法 で記述する
  • ただし、最初の仮引数に self を記述 する点が 異なる
  • self には、メソッドを呼び出した インスタンスが代入 される
  • メソッドは インスタンス.メソッド名 のように記述する
  • メソッドの呼び出しは、関数の呼び出しと同様の方法 で記述する
  • ただし、実引数 は、self の後 に記述された仮引数に対して 順番に代入 される

place_markdisplay_board メソッドの定義

place_markdisplay_board メソッド は下記のプログラムのように定義します。これまでに 関数として定義 した place_markdisplay_board との 違い は以下の通りです。

  • クラスのブロックに記述する
  • 最初の仮引数に self を記述する
  • 仮引数 board を削除 する
  • ブロックの中の boardself.board に変更 する
  • メソッドの呼び出しの際に記述した 実引数 は、self の次の仮引数 から 順番に代入 される

具体的な違いについては、下記の修正箇所の部分をクリックして下さい。

class Marubatsu:
    def initialize_board(self):
        self.board = [["."] * 3 for y in range(3)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == ".":
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        # 各行に対する繰り返しの処理を行う
        for y in range(3):
            # y 行の各マスに対する繰り返しの処理を行う
            for x in range(3):
                # (x, y) のマスの要素を改行せずに表示する
                print(self.board[x][y], end="")
            # x 列の表示が終わったので、改行する
            print()  
修正箇所
class Marubatsu:
-   def initialize_board():
+   def initialize_board(self):
-       return [["."] * 3 for y in range(3)]
+       self.board = [["."] * 3 for y in range(3)]

-   def place_mark(board, x, y, mark):
+   def place_mark(self, x, y, mark):
-       if board[x][y] == ".":
+       if self.board[x][y] == ".":
-           board[x][y] = mark
+           self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

-   def display_board(board):   
+   def display_board(self):   
        # 各行に対する繰り返しの処理を行う
        for y in range(3):
            # y 行の各マスに対する繰り返しの処理を行う
            for x in range(3):
                # (x, y) のマスの要素を改行せずに表示する
-               print(board[x][y], end="")
+               print(self.board[x][y], end="")
            # x 列の表示が終わったので、改行する
            print()  

最初の仮引数に self を記述する理由と意味については先ほど説明しました。

仮引数 board を削除 したのは、initialize_board メソッドを実行することによって、インスタンスの board 属性 にゲーム盤のデータが 代入される からです。仮引数 self に、メソッドを呼び出した インスタンスが代入 されるので、self.boardゲーム盤のデータ を表します。従って、boardself.board に変更 することで、関数の場合と 同じ処理を行う ことが出来ます。

上記のクラスを定義する事で、下記の 2 ~ 5 行目のプログラムのように、上記で定義したメソッドの呼び出しを記述することが出来ます。3 行目の place_mark(0, 1, "o") のメソッドの呼び出しで記述した 実引数 は、self の次の仮引数 である xymark順に 代入されます。

実行結果から、(0, 1) のマスに 〇 が、(1, 2) のマスに × のマークが正しく配置されることが確認できます。

mb = Marubatsu()
mb.initialize_board()
mb.place_mark(0, 1, "o")
mb.place_mark(1, 2, "x")
mb.display_board()

実行結果

...
o..
.x.

異なるインスタンスが独立したデータであることの確認

次に、Marubatsu から 複数のインスタンスを作成 した際に、それらのインスタンスが 独立したデータ であることを 確認 することにします。

下記のプログラムは、2、8 行目で、Marubatsu のインスタンスを作成し、それぞれを mb1mb2 に代入しています。3、9 行目でそれぞれのゲーム盤を初期化した後で、4 行目で mb1 の (0, 1) のマスに 〇 を、10 行目で mb2 の (1, 2) のマスに × のマークを配置しています。5、11 行目の実行結果から、mb1mb2board 属性に、異なるゲーム盤のデータが代入 されていることを確認することが出来ます。なお、1、7 行目の print は、表示する 2 つのゲーム盤の 境目をわかりやすくする ための表示です。

 1  print("mb1")
 2  mb1 = Marubatsu()
 3  mb1.initialize_board()
 4  mb1.place_mark(0, 1, "o")
 5  mb1.display_board()
 6
 7  print("mb2")
 8  mb2 = Marubatsu()
 9  mb2.initialize_board()
10  mb2.place_mark(1, 2, "x")
11  mb2.display_board()
行番号のないプログラム
print("mb1")
mb1 = Marubatsu()
mb1.initialize_board()
mb1.place_mark(0, 1, "o")
mb1.display_board()

print("mb2")
mb2 = Marubatsu()
mb2.initialize_board()
mb2.place_mark(1, 2, "x")
mb2.display_board()

実行結果

mb1
...
o..
...
mb2
...
...
.x.

クラスを使うメリット

place_markdisplay_boardクラスのメソッド として定義した場合と、通常の関数 で定義した場合の 違い は、メソッドの場合は 〇×ゲームに 関するデータが代入された変数 board実引数に記述する必要がない という点です。

上記のプログラムと、同じ処理を行う、これまでに定義した関数を使ったプログラムを 比較 して下さい。

from marubatsu import place_mark, display_board

print("board1")
board1 = initialize_board()
place_mark(board1, 0, 1, "o")
display_board(board1)

print("board2")
board2 = initialize_board()
place_mark(board2, 1, 2, "x")
display_board(board2)

実行結果

board1
...
o..
...
board2
...
...
.x.

現時点では〇×ゲームに 関連するデータ が代入された 変数 は、ゲーム盤のデータを代入する board のみであるため、あまり恩恵を感じられないかもしれませんが、クラスを利用せず に〇×ゲームのプログラムを記述する場合は、今後「手番」や「勝敗結果」などを表す〇×ゲームに関する データが増えた場合 に、〇×ゲームの処理を行う 関数の仮引数が増えていく可能性が高く なります。

一方、クラス を使って〇×ゲームの処理を行う場合は、インスタンスの属性〇×ゲームに関連するデータすべて保存 されるようにしておけば、メソッドの 実引数 に〇×ゲームに関するデータを記述する 必要が無くなります

また、クラスを使わない場合は、変数や関数が 増えた場合 にそれらの 名前の関連性がわかりづらく なりますが、クラスを使った場合は、関連する変数と関数 が、インスタンスの 属性とメソッドまとめられる ので、それらの 関連性が明確 になるというメリットがあります。

現時点では上記のメリットをあまり実感できないかもしれませんが、今後〇×ゲームの実装をクラスを使って進めていくことで、上記のメリットが徐々に実感できるようになるのではないかと思います。

特殊属性

クラスの定義を実行した際に作られるクラスオブジェクトや、クラスから作成されたインスタンスオブジェクトには、クラスの定義記述されていない特殊属性 が自動的に作成されます。

特殊属性の名前は、前後に 半角のアンダースコア _ を 2 つ並べた __ が付けられるという決まりになっています。この __ のことを、ダブルアンダースコア5 と呼びます。

特殊属性の名前の前後に __ が付けられるのは、他の目的 でそのような名前を 定義する事がありそうにない からです。

特殊属性をいくつか紹介します。他の特殊属性については必要になった時点で説明します。

__name__

関数オブジェクト、クラスオブジェクト、メソッドオブジェクトなどで利用できる特殊属性で、関数、クラス、メソッドなどの 名前を表す文字列 が代入されます。

単に __name__ と記述した場合は、そのプログラムが記述されている モジュールの名前 が代入された特殊属性を表します。ただし、メインモジュール の場合は、__main__ という 文字列 が代入されます。

__name__ 属性は、実行した文メインモジュールのプログラム であるかどうかを 判定 したい場合などで良く使われます。その使用法の具体例については必要になった時点で説明します。

def add(x, y):
    """ x + y を計算する関数 """
    return x + y

print(add.__name__)  # add の関数名を表示する
print(__name__)      # この文を実行したモジュールの名前を表示する

実行結果

add
__main__

__dict__

クラスやインスタンスなどで利用できる特殊属性で、クラスインスタンス の場合は、名前空間 を表す dict が代入されます。詳細は次回の記事で説明します。

下記のプログラムは、Marubatsu クラスの名前空間 を表示しています。実行結果の意味については次回の記事で説明します。

print(Marubatsu.__dict__)

実行結果

{'__module__': '__main__', 'initialize_board': <function Marubatsu.initialize_board at 0x0000021A6E950CC0>, 'place_mark': <function Marubatsu.place_mark at 0x0000021A6E9518A0>, 'display_board': <function Marubatsu.display_board at 0x0000021A6E9520C0>, '__dict__': <attribute '__dict__' of 'Marubatsu' objects>, '__weakref__': <attribute '__weakref__' of 'Marubatsu' objects>, '__doc__': None}

特殊メソッド

クラスの定義 を記述する際に、特殊メソッド という 特殊な処理を行うメソッドを定義 することが出来ます。特殊メソッドの 名前 は、特殊属性と同様 に、前後に __ が付けられます。

クラスの定義の中に、特殊メソッドの定義を記述 することで、クラスから作成した インスタンス に対して 様々な機能 を持たせることが出来るようになります。なお、特殊メソッドによって得られる機能が 必要ない 場合は、クラスの定義の中に特殊メソッドの定義を記述する 必要はありません

今回の記事では、__init__ という名前の特殊メソッドについて説明します。他の特殊メソッドについては、必要になった時点で紹介します。

クラスの特殊メソッドの一覧と詳細に関しては下記のリンク先を参照して下さい。

__init__

先程定義した Marubatsu クラスから作成されたインスタンスは、initialize_board メソッドを呼び出す ことで board 属性 にゲーム盤のデータが 代入される ので、下記のプログラムのように、initialize_board メソッドを 呼び出す前 に、place_markdisplay_board メソッドを 呼び出す と、board 属性値が代入されていない ので、エラーが発生 します。

mb = Marubatsu()
mb.place_mark(0, 1, "o")
mb.display_board()

実行結果

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\019\marubatsu.ipynb セル 23 line 2
      1 mb = Marubatsu()
----> 2 mb.place_mark(0, 1, "o")
      3 mb.display_board()

c:\Users\ys\ai\marubatsu\019\marubatsu.ipynb セル 23 line 6
      5 def place_mark(self, x, y, mark):
----> 6     if self.board[x][y] == ".":
      7         self.board[x][y] = mark
      8     else:

AttributeError: 'Marubatsu' object has no attribute 'board'

ゲーム盤の初期化処理は 最初に必ず行うべき処理 なので、インスタンスを 作成した後自動的に initialize_board処理が行われる ようになると 便利 です。

Python のクラスには、インスタンスを作成した後で 自動的に実行 される __init__ という名前 の、イニシャライザ (初期化子) と呼ばれる 特殊メソッドを定義 することが出来ます。

下記のリンク先の python の公式ドキュメントでは、__init__ には特に名前は付けられていないようですが、筆者が調べた範囲ではイニシャライザと呼ばれる場合が多いような気がしましたので、本記事でもそのように表記します。

なお、インスタンスを作成した後で自動的に実行したい処理が 存在しない場合 は、クラスの定義の中で __init__ の定義を記述する 必要はありません

__init__ のことを、コンストラクタであると説明する場合が多いようです。コンストラクタ(constructor)とは、「建築」や「組み立て」を表す construct の名詞形で、クラスからインスタンスを作成する(組み立てる)際に実行されるメソッドの事を表します。Python の __init__ は確かにコンストラクタと 似ています が、厳密には異なる処理を行う ので、__init__ のことをコンストラクタという名前で 呼ぶのは正確ではありません

Python では、コンストラクタに相当する処理を行う __new__ という名前の特殊メソッドがありますが、__new__ を使わなければならない場合は あまり多くない ので、一般的には __init__ のほうが使われます。本記事でも __new__ は使用しません。

なお、Python の公式ドキュメントでは、クラスからインスタンスを作成する際に記述する Marubatsu() のような式の事を、コンストラクタ式と呼ぶようです。

下記のプログラムは、Marubatsu クラスの定義の中で、initialize_board メソッドを呼び出す処理 を行う特殊メソッド __init__ を定義 しています。

class Marubatsu:
    def __init__(self):
        self.initialize_board()

    def initialize_board(self):
        self.board = [["."] * 3 for y in range(3)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == ".":
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        # 各行に対する繰り返しの処理を行う
        for y in range(3):
            # y 行の各マスに対する繰り返しの処理を行う
            for x in range(3):
                # (x, y) のマスの要素を改行せずに表示する
                print(self.board[x][y], end="")
            # x 列の表示が終わったので、改行する
            print()  

上記のように __init__ メソッドを定義する事で、Marubatsu のインスタンスを作成すると、__init__ メソッドが自動的に呼び出され、その中で initialize_board メソッドが呼び出される ようになります。従って、下記のプログラムの 2 行目のように、Marubatsu クラスからインスタンスを 作成した直後mb.place_mark メソッドを呼び出しても エラーは発生しなくなります

mb = Marubatsu()
mb.place_mark(0, 1, "o")
mb.display_board()

実行結果

...
o..
...

インスタンスを作成した直後に 初期設定 を行いたい場合は、__init__ メソッドを定義 する。

__init__ メソッドの仮引数

__init__ メソッドの定義には self 以外の仮引数を記述 することが出来、インスタンスを作成する際に () の中に記述した 実引数が それらの 仮引数に代入 されます。

具体例を挙げます。下記のプログラムは、self の次に x という仮引数 を持つ __init__ メソッドが定義された A というクラスを定義しています。5 行目でクラス A のインスタンスを作成する際に、() の中の実引数に 5 を記述することで、__init__ メソッドの 仮引数 x5 が代入 されるので、インスタンスの 属性 x5 が代入 されます。従って、6 行目で a.x を表示すると 5 が表示されます。

1  class A:
2      def __init__(self, x):
3        self.x = x
4
5  a = A(5)
6  print(a.x)
行番号のないプログラム
class A:
    def __init__(self, x):
        self.x = x

a = A(5)
print(a.x)

実行結果

5

他の例としては、組み込み型のデータを int(5) のように、組み込み型のクラスに実引数を記述して呼び出すという方法で記述する際にこの仕組みが使われています。

Marubatsu クラスの __init__ メソッドに仮引数を記述する例については次回の記事で紹介します。

クラスに対する docstring

クラスに対する docstring は以下のように記述します。下記では省略していますが、クラスの中で定義されるメソッドの docstring は 通常の関数と同様の方法 で記述します。ただし、メソッドの仮引数 self の意味 はどのメソッドでも 共通 なので、self に関する説明は省略 します。

class クラス名:
    """ クラスの概要

    クラスの詳細な説明

    Attributes:
        属性名 (属性のデータ型):
            属性の説明

下記は、Marubatsu クラスの定義に docstring を記述したプログラムです。

class Marubatsu:
    """ 〇×ゲーム.

    Attributes:
        board (list[list[str]):
            ゲーム盤を表す 2 次元配列の list
            2 つのインデックスは、順に x 座標、y 座標を表す
            空白のマスは "."、〇 のマスは "o"、× のマスは "x" を代入する
    """

    def __init__(self):
        """ イニシャライザ """

        # ゲーム盤の初期化を行うメソッドを呼び出す
        self.initialize_board()

    def initialize_board(self):
        """ ゲーム盤のデータの初期化. """

        self.board = [["."] * 3 for y in range(3)]

    def set_mark(self, x: int, y: int, mark: str):
        """ ゲーム盤の指定したマスに指定したマークを配置する.

        (x, y) のマスに mark で指定したマークを配置する.
        (x, y) のマスに既にマークが配置済の場合は、メッセージを表示する.

        Args:
            x:
                マークを配置するマスの x 座標
            y:
                マークを配置するマスの y 座標
            mark:
                配置するマークを表す文字列
        """

        if self.board[x][y] == ".":
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        """ ゲーム盤の表示. """

        # 各行に対する繰り返しの処理を行う
        for y in range(3):
            # y 行の各マスに対する繰り返しの処理を行う
            for x in range(3):
                # (x, y) のマスの要素を改行せずに表示する
                print(self.board[x][y], end="")
            # x 列の表示が終わったので、改行する
            print()   

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

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

以下のリンクは、今回の記事で更新した marubatsu_new.py です。

次回の記事

  1. 「使える記号は _ のみ」、「先頭にアラビア数字を使うことはできない」など

  2. 「事例」を表す英単語です

  3. 例えば、[1] * 5 のように、list と整数型のデータを * 演算子で演算することができます

  4. 「オーダーメイドの」、「注文して作った」を表す custom という英単語です

  5. 英語では、double underscore。略してダンダー(dunder)と呼ぶこともあるようです

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