0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その191 ポリモーフィズムによる異なるゲーム盤を表すデータ構造の切り替えと __getitem__ と __setitem__ の利用方法

Last updated at Posted at 2025-09-05

目次と前回の記事

Python のバージョンとこれまでに作成したモジュール

本記事のプログラムは Python の バージョン 3.13 で実行しています。

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
mbtest.py テストに関する関数
util.py ユーティリティ関数の定義
tree.py ゲーム木に関する Node、Mbtree クラスなどの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

異なるゲーム盤を表すデータ構造の切り替え

現状の Marubatsu クラスは、下記の ゲーム盤の初期化 を行う initialize_board の中で記述されているように、2 次元の list でゲーム盤のデータを表現してきました。

    def initialize_board(self):
        self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]

このデータ構造は (x, y) のマスboard[x][y] のように わかりやすく記述できる という利点がありますが、処理速度 の面では 最善のデータ構造ではありません。ゲーム盤のデータは 様々なデータ構造で表現 することができ、データ構造 とそれを処理する アルゴリズム によって 処理速度が大きく変わります。そこで、本記事から ゲーム盤を表現するデータ構造をいくつか紹介 し、それらの 性質や処理速度などを比較 することにします。

ゲーム盤のデータ構造の切り替え

様々なデータ構造 でゲーム盤を表現する際に、それらの 性質やその処理速度を比較 するためには Marubatsu クラスがそれぞれのゲーム盤のデータ構造を 切り替えて利用できる ようにする必要があります。同様の切り替え前回の記事place_mark メソッドと remove_mark メソッドで 座標のチェックを行うか どうかの切り替えを行う場合などで行ってきましたが、それと同じ方法ゲーム盤のデータ構造の切り替えを行う 場合は、Marubatsu クラスのメソッドを下記のプログラムのように修正する必要があります。

  • 1、3 行目__init__ メソッドにゲーム盤のデータ構造を表す文字列を代入する仮引数 boardtype を追加し、同名の属性に代入する。下記では従来の 2 次元の list でゲーム盤を表す文字列を "list" とし、それをデフォルト値とした
  • 6 ~ 12 行目:if 文で boardtype 属性に代入された文字列に応じたゲーム盤のデータを board 属性に代入する。下記の 8 ~ 12 行目では仮に "typeA"、"typeB" という文字列で表されるゲーム盤のデータ構造があるものとしてプログラムを記述した
  • 17 ~ 26 行目:if 文で boardtype 属性に代入された文字列に応じた (x, y) のマスにマークを配置する処理を記述する
 1  def __init__(self, , boardtype="list"):
 2      
 3      self.boardtype = boardtype
 4
 5  def initialize_board(self):
 6      if self.boardtype == "list":
 7          self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
 8      elif self.boardtype == "typeA":
 9          self.board = typeA のゲーム盤のデータの初期化処理
10      elif self.boardtype == "typeB":
11          self.board = typeB のゲーム盤のデータの初期化処理
12      以下ゲーム盤のデータ構造の種類だけ記述する
13
14  def place_mark(self, x: int, y: int, mark: str) -> bool:
15      if self.check_coord:
16          if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
17              if self.boardtype == "list":
18                  if self.board[x][y] == Marubatsu.EMPTY:
19                      self.board[x][y] = mark
20                      return True
21                  else:
22                      print("(", x, ",", y, ") のマスにはマークが配置済です")
23                  return False
24              elif self.boardtype == "typeA":
25                  typeA のゲーム盤の (x, y) にマークを配置する処理
26              以下ゲーム盤のデータ構造の種類だけ記述する

typeA のような抽象的な説明ではピンとこない人は、具体例として 1 次元の list でゲーム盤のデータを表現する方法を今回の記事で紹介しているのでそちらを見て下さい。また、後で説明するように 1 次元の list でゲーム盤を表現する場合は (x, y) のマスself.board[x][y] とは 異なる方法で記述 する必要があります。

また、上記以外の場所 でもゲーム盤に対して下記の処理を行う際に if 文などでゲーム盤の種類ごとに異なる処理を記述 する必要が生じます。

  • ゲーム盤の 特定のマス の内容を 参照 する
  • ゲーム盤の 特定のマス の内容を 変更 する

そのため、ゲーム盤のデータ構造の 種類が増える とそれに 対応する処理 を Marubatsu クラスの 複数の場所に記述 する必要が生じ、面倒なだけでなくバグが発生しやすい という問題が発生します。この問題の解決方法の一つに、ポリモーフィズム があります。

ポリモーフィズム

ポリモーフィズム(polymorphism。多態性)は プログラム言語の概念 の一つで、Wikipedia には「それぞれ異なる型に一元アクセスできる共通接点の提供」のように説明されています。ポリモーフィズムには いくつかの種類 があり、プログラム言語によって ポリモーフィズムを 実現する方法が大きく異なる 点がわかりづらいのですが、Python でのポリモーフィズム比較的簡単な方法で実現 されています。Python でのポリモーフィズム を簡単に説明すると、異なるデータ型を扱うクラス共通するメソッドを持つ ようにすることで、それらのクラスのインスタンスを 共通して扱うことができるようにする ことを意味します。

ゲーム盤のデータ構造 に対する ポリモーフィズム は、以下のような方法で実現します。

  • ゲーム盤に対する 処理を行うために必要な処理 を考えて 列挙する
  • ゲーム盤のデータ構造を表すクラス を、上記で 列挙した処理 を行う 共通の名前のメソッドを持つ ように定義する

ゲーム盤に対する処理を行うために 必要なメソッドが共通の名前で定義 されていれば、ゲーム盤の データ構造が異なっていて同じ記述方法で処理を行うことができる ようになるため、先程のプログラムは下記のプログラムのように 簡潔に記述できる ようになります。修正する部分は下記のプログラムだけ で、他のプログラムの変更を行う必要はありません。

  • 1、3 行目__init__ メソッドに ゲーム盤のデータ構造を表すクラス を代入する仮引数 boardclass を追加して同名の属性に代入する。下記ではこれまでの 2 次元の list でゲーム盤のデータを表す ListBoard という クラスが定義されている ものとする。ListBoard の定義の方法については後述する
  • 6 行目boardclass に代入された クラスのインスタンスを作成 することで ゲーム盤のデータを作成 して board 属性に代入 する
 1  def __init__(self, , boardclass=ListBoard):
 2      
 3      self.boardtype = boardclass
 4
 5  def initialize_board(self):
 6      self.board = boardclass()

このように、ポリモーフィズムを利用することで、同じ機能を持つ異なるデータ型 のデータを 複数扱う 必要がある際に データ型とその機能を定義するクラス と、そのデータ型の データを扱う側のプログラム独立して記述 することができるようになり、プログラムの 柔軟性生産性が向上する という利点が得られます。

この説明ではピンとこない人が多いと思いますので、複数の 〇× ゲームの ゲーム盤のデータポリモーフィズムを利用して実装 するという具体例をあげながら説明します。

なお、Python では ポリモーフィズムが頻繁に利用 されており、for 文 に list だけでなく、tuple、set、dict などの 反復可能オブジェクト を記述できる のもポリモーフィズムの仕組みによるものです。具体的には、反復可能オブジェクトに分類 されるオブジェクトには、for 文による繰り返し処理 の際に 次の要素を取り出す ために呼び出される __iter__ またはシーケンス として実装された __getitem__ という特殊メソッドが定義されています1

他にも print で様々なオブジェクトを 文字で表示できる のは、オブジェクトの内容を 文字列に変換 する __str__ というメソッドが定義されているからです2

反復可能オブジェクト(iterable)の詳細に関しては下記のリンク先を参照して下さい。

現実の例でポリモーフィズムに 似たもの としては 電池 などの 部品の規格 が挙げられると思います。例えば 単一電池 にはマンガン電池、アルカリ電池、充電可能な電池のように いくつかの種類 がありますが、単一電池の 規格で決められた以下の点が必ず共通 しています。

  • 電池の形状
  • プラス極とマイナス極をつなぐことで 1.5 V の電圧が発生する

上記の 規格を満たせば、電池の 中身がどのようなものであっても 単一電池を必要とする電化製品で 利用することが可能 です。例えば、電池のアダプターを利用することで形状が異なる単三電池を単一電池として利用する事さえできます3。他にもインクのカートリッジ、様々なテレビで利用できるリモコンなど、決められた規格に従って作られたもの はその 規格にあう製品に対して利用 することができます。また、いずれの場合でも、その際に 製品のほうに対して 何らかの 調整を行う必要は一切ありません

参考までに Wikipedia のポリモーフィズムのリンクを下記に示します。

Python のように、必要なメソッドを共通して持つことで実現するポリモーフィズムのことをダックタイピングと呼び、Python のドキュメントには「あるオブジェクトが正しいインターフェースを持っているかを決定するのにオブジェクトの型を見ないプログラミングスタイルです」のように説明されています。

ダックタイピングは "If it walks like a duck and quacks like a duck, it must be a duck"(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)という「ダックテスト」と呼ばれる推論の方法が由来で、「違いない」という説明からわかるように、推測が間違っていた場合はバグの原因になることがあります。例えば偶然必要なメソッドを持つ全く異なるデータ型の処理を行うことができてしまい、それが原因でバグが発生する可能性があります。そのため、より厳密な方法でポリモーフィズムを実現しているプログラム言語が多数あります。

一方でダックタイピングは厳密ではない代わりに、プログラミングを柔軟に記述できるという利点があります。他にも様々な利点と欠点があるので興味がある方は調べてみて下さい。

参考までにダックタイピングのリンクを下記に示します。

ゲーム盤のデータ構造を表すクラスに必要なメソッド

Python でゲーム盤のデータに対するポリモーフィズムを実現するためには、ゲーム盤を表すデータ に対して 行う必要がある処理を列挙 する必要があります。ゲーム盤のデータは board 属性に代入 されており、これまでに記述してきたプログラムで board 属性に対して行う処理 は以下の 2 種類だけです。なお、本記事では以後は board[x][y] のように、list や dict などの後ろに記述する [] とその中身 の事を 添字 と表記することにします。

  • 式の中で board[x][y] のように、2 つの添字 を記述して (x, y) のマスの中身を 参照 する
  • board[x][y] = Marubatsu.CIRCLE のように 2 つの添字 を記述したものに 値を代入 することで、(x, y) のマスに 値を設定 する

従って、ゲーム盤を表すデータ上記の方法で特定のマスに対する参照と代入 を行うことができれば、ポリモーフィズムを実現することができます。

list や dict などのデータ型では 後ろに添字を記述 することで 特定の要素の参照と代入 を行うことができますが、同様の処理 をクラスに __getitem____setitem__ という 特殊メソッドを定義 する事で、そのクラスの インスタンスに対して行う ことができます。これらの特殊メソッドの定義の方法については後述します。

従って、ゲーム盤のデータ構造を表すクラス には少なくとも __getitem____setitem__ メソッドが必要 であることがわかりました。他にも必要なメソッドがあるかもしれませんが、必要であることがわかり次第、その都度 追加、修正していく ことにします。

抽象クラスの定義

Python でポリモーフィズムを実現 する際には、以前の記事で説明した 抽象クラスを定義 し、その抽象クラスから 継承する ことでクラスを定義するのが一般的です。また、その抽象クラスでは、共通して必要となるメソッド抽象メソッド として定義します。

以前の記事で説明したように、抽象メソッドが定義された 抽象クラスを継承したクラス は、抽象メソッドを定義しないと インスタンスの作成時に エラーが発生 するので、必要なメソッド定義のし忘れを防ぐ ことができます。

以前の記事では 〇× ゲームに関する様々な GUI を作成する際の基底クラスとして利用するために抽象クラスを定義しましたが、抽象クラスはポリモーフィズムを実現するためにも利用することができます。

そこで、〇× ゲームの ゲーム盤のデータ構造を定義 するための 基底クラス として Board という名前の 抽象クラス を下記のプログラムのように定義することにします。__getitem____setitem__ メソッドの仮引数についてはこの後で説明します。

from abc import ABCMeta, abstractmethod

class Board(metaclass=ABCMeta):
    @abstractmethod
    def __getitem__(self, key):
        pass

    @abstractmethod
    def __setitem__(self, key, value):
        pass

__getitem__ メソッドの利用方法

次に __getitem____setitem__ メソッドについて説明します。__getitem__ メソッドが定義されたクラス から作成されたインスタンスは 添字を 1 つ記述できる ようになり4添字を記述した場合 は下記の処理が行われます。

  • __getitem__ メソッドが呼び出され、仮引数 key に添字の値が代入される
  • __getitem__ メソッドの返り値で置き換えられる

具体例を挙げます。下記のプログラムは 常に key の値を返り値 として返す __getitem__ メソッドだけが定義 された A というクラスを定義しています。

class A:
    def __getitem__(self, key):
        return key

そのため、下記のプログラムのように A のインスタンスを作成し、添字を 1 つ記述して print で表示 すると 添字の値が key に代入 されて __getitem__ が呼び出され、key の値をそのまま返り値として返す 処理が行われるので、実行結果のように 添字に記述した値がそのまま表示 されます。

a = A()
print(a[100])
print(a["abc"])

実行結果

100
abc

上記の A のインスタンスのように属性が一つも存在せず、何のデータも記録していない場合 でも __getitem__ を定義 することができます。ほとんどのクラス では 添字の記述 によってインスタンスが 記録するデータの一部が参照 されますが、上記のように 記録するデータと関係のない値 が返り値となる場合があります。

下記のようなクラスを作成する意味は特にありませんが、list と同じようなデータ構造 を持つ 自作の Mylist クラスを下記のように定義することができます。

  • 2、3 行目:仮引数 data を持つ __init__ メソッドを定義し、同名の属性に data を代入する。なお、data には list が代入されることを想定している
  • 5、6 行目self.data[key] を返り値として返す処理を行う __getitem__ を定義する
1  class Mylist:
2      def __init__(self, data):
3          self.data = data
4          
5      def __getitem__(self, key):
6          return self.data[key]
行番号のないプログラム
class Mylist:
    def __init__(self, data):
        self.data = data
        
    def __getitem__(self, key):
        return self.data[key]

下記のプログラムを実行して Mylist のインスタンスを作成 すると、[1, 2, 3, 4, 5] という list が ldata 属性に代入 され、l[0] を記述することで l.data[0] が返り値として返りますlist と同様l[2]l[1:3]l.data[2]l.data[1:3] の値も参照できます。

l = Mylist([1, 2, 3, 4, 5])
print(l[0])
print(l[2])
print(l[1:3])

実行結果

1
3
[2, 3]

Mylist は list とは異なり下記のプログラムの実行結果の 1 行目のように print(l) を実行しても list の内容は表示されません5。また、後述の __setitem__ メソッドが定義されていな いので、list とは異なり l[0] = 2 のような 添字による代入処理 を行うと実行結果のように エラーが発生 します。このように、list と全く同じ機能を持つようにするためには Mylist に多くの特殊メソッドを定義する必要があります。

print(l)
l[0] = 2

実行結果

<__main__.Mylist object at 0x000001579E1FEF90>

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 2
      1 print(l)
----> 2 l[0] = 2

TypeError: 'Mylist' object does not support item assignment

__getitem__ メソッドの詳細については下記のリンク先を参照して下さい。

__setitem__ メソッドの利用方法

__setitem__ メソッドが定義されたクラスから作成されたインスタンスは、添字を記述した場合に 代入を行える ようになり、そのような記述を行った場合は下記の処理が行われます。

  • __setitem__ メソッドが呼び出され、仮引数 key に添字に記述した値が代入され、value に代入文の式の値が代入される
  • __getitem__ とは異なり、return 文による返り値を返す必要はない

下記は先ほどの Mylist クラスに __setitem__ メソッドを追加して、list のように 添字を記述 して data 属性の添字の要素に値を代入 できるようにしたプログラムです。

  • 8、9 行目data 属性に代入された list の key の要素に value を代入する処理を行う __setitem__ を定義する
1  class Mylist:
2      def __init__(self, data):
3          self.data = data
4          
5      def __getitem__(self, key):
6          return self.data[key]
7      
8      def __setitem__(self, key, value):
9          self.data[key] = value
行番号のないプログラム
class Mylist:
    def __init__(self, data):
        self.data = data
        
    def __getitem__(self, key):
        return self.data[key]
    
    def __setitem__(self, key, value):
        self.data[key] = value

上記の修正によって、下記のプログラムの 3 行目のように l[0] = 5 のような 添字を記述した代入処理 によって l.data[0] = 5 という代入処理が行われる ようになります。そのことは、実行結果の l[0]l.data の表示から確認することができます。

l = Mylist([1, 2, 3, 4, 5])
print(l[0])
l[0] = 5
print(l[0])
print(l.data)

実行結果

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

__setitem__ メソッドの詳細については下記のリンク先を参照して下さい。

ListBoard クラスの定義

ここまでの内容を元に 2 次元の list でゲーム盤を表す ListBoard クラスを定義 することにします。ゲーム盤のデータは board 属性 に代入し、__init__ メソッドでその 初期化を行う ことにします。下記は ListBoard の定義です。ゲーム盤のサイズを変更できる ように __init__ メソッドには 仮引数 board_size を用意しました。継承した Board クラス抽象メソッドは必ず定義する必要がある ので、__getitem__ メソッドと __setitem__ メソッドは 何も行わないメソッドとして定義 し、後で修正する ことにします。

  • 4、5 行目:デフォルト値を 3 とする仮引数 board_size を持つ __init__ メソッドを定義し、同名の属性に board_size を代入する
  • 6 行目board 属性に Marubatsu クラスの initialize_board と同じ方法で 2 次元の list でゲーム盤のデータを初期化する
 1  from marubatsu import Marubatsu
 2  
 3  class ListBoard(Board):
 4      def __init__(self, board_size=3):
 5          self.BOARD_SIZE = board_size
 6          self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
 7          
 8      def __getitem__(self, key):
 9          pass
10      
11      def __setitem__(self, key, value):
12          pass
行番号のないプログラム
from marubatsu import Marubatsu

class ListBoard(Board):
    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
        self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
        
    def __getitem__(self, key):
        pass
    
    def __setitem__(self, key, value):
        pass

上記の定義後に下記のプログラムで ListBoard のインスタンスを作成 し、board 属性を表示 すると実行結果のようにゲーム盤を表す 2 次元の list が代入される ことが確認できます。

board = ListBoard()
print(board.board)

実行結果

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

__getitem__ メソッドと ListBoardwithFirstkey クラスの定義

次に、board[x][y] を記述することで (x, y) のマスのマークを参照できる ように __getitem__ メソッドを定義 する必要がありますが、__getitem__ メソッドは 1 つの添字にしか対応していない ため、board[x][y] のような 2 つの添字の記述に対応6するためは 工夫を行なう必要 があります。本記事ではその工夫の一つを紹介しますが、もっと良い方法があるかもしれませんので、ご存じの方がいればコメントなどで教えて頂ければ助かります。

その方法は、下記のような クラスを定義 し、ListBoard クラスの __getitem__ メソッドが そのクラスのインスタンスを返す ようにするというものです。

  • ゲーム盤のデータを表す board 属性1 つ目の添字の値を記録 する
  • __getitem__ メソッドを定義し、その中で 記録した 1 つ目の添字 の値と、仮引数 key に代入された 2 つ目の添字 を用いて 2 つの添字に対応するマスの値を返り値として返す処理を行う

上記のクラスは ListBoard のゲーム盤のデータ最初の添字の値 を記録するので ListBoardwithFirstkey という名前にし、下記のプログラムのように定義します。

  • 2 ~ 4 行目ゲーム盤のデータ と、1 つ目の添字の値 を代入する仮引数 boardkey を持つ __init__ メソッドを定義し 同名の属性に代入 する
  • 6、7 行目1 つ目の添字の値 を表す self.key2 つ目の添字の値 を表す key を用いて (self.key, key) のマスの値を返り値として返す __getitem__ メソッドを定義する
1  class ListBoardwithFirstkey:
2      def __init__(self, board, key):
3          self.board = board
4          self.key = key
5          
6      def __getitem__(self, key):
7          return self.board[self.key][key]
行番号のないプログラム
class ListBoardwithFirstkey:
    def __init__(self, board, key):
        self.board = board
        self.key = key
        
    def __getitem__(self, key):
        return self.board[self.key][key]

ListBoardwithFirstkey のインスタンスは ゲーム盤のデータ の中の key 列目を表す list のような 機能を持つ ため、添字を記述 することで、key添字に記述した行 のデータを 参照することができます

ListBoard__getitem__ メソッドは下記のプログラムのように、ゲーム盤のデータ である self.board1 つ目の添字の値 を表す key を実引数に記述して ListBoardwithFirstkey のインスタンスを作成 し、その インスタンスを返す ように定義します。

def __getitem__(self, key):
    return ListBoardwithFirstkey(self.board, key)

ListBoard.__getitem__ = __getitem__

上記の定義を行うことで、下記のプログラムのように ListBoard のインスタンス に対して board[0][1] のような 2 つの添字を記述 することで、board.board[0][1] に代入された 2 次元の list から ゲーム盤の 2 つの添字の座標のマスの値を参照 できるようになります。

board = ListBoard()
print(board[0][1])

実行結果

.

board[0][1] を記述した際に行われる処理

おそらくここまでの説明を読んで、上記で行われる処理が理解できない人が多いのではないかと思いますので、board[0][1] を記述した際に 行われる処理について詳しく説明 します。

board[0][1] のような 2 つ以上の添字が記述 されている場合の処理は、添字に対する処理が一度に行われるのではなく、下記の手順で 先頭の添字から順番に処理 が行われます。

  1. board[0] に対する __getitem__ の処理 が行われる
  2. 上記の __getitem__ の返り値 に対して、次の添字 である [1] に対する __getitem__ の処理 が行われる

そのことは、下記のプログラムのように board[0]b0 に代入 し、b0[1]print で表示 することで、print(board[0][1]) と同じ結果が表示 されることからも確認できます。

b0 = board[0]
print(b0[1])

実行結果

.

上記の b0 に代入 された baord[0] は、下記の ListBoard クラスの __getitem__ メソッドの 返り値の値 になります。

def __getitem__(self, key):
    return ListBoardwithFirstkey(self.board, key)

その際に仮引数 key には 添字の値 である 0 が代入 されるので、__getitem__ の返り値は下記のようになります。

ListBoardwithFirstkey(self.board, 0)

ListBoardwithFirstkey クラスの __init__ メソッドは下記のように定義されているので、board 属性に ゲーム盤を表す 2 次元の list が、key 属性に 1 つ目の添字 の値である 0 が代入 された ListBoardwithFirstkey クラスのインスタンスが b0 に代入されます。

def __init__(self, board, key):
    self.board = board
    self.key = key

そのことは下記のプログラムで確認することができます。なお、ListBoardwithFirstkey には __str__ メソッドが定義されていない ので、b0print で表示 すると下記の実行結果の 1 行目のような表示が行われますが、その中に ListBoardwithFirstkey クラスのインスタンスである ことを表す __main__.ListBoardwithFirstkey が表示されます。

print(b0)
print(b0.board)
print(b0.key)

実行結果

<__main__.ListBoardwithFirstkey object at 0x000002897F6D8190>
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
0

この b0 に対して b0[1] のように 1 という添字を記述 すると、下記の ListBoardwithFirstkey クラスの __getitem__ メソッドが呼び出されます。

def __getitem__(self, key):
    return self.board[self.key][key]

また、上記の return 文のそれぞれの変数の値は下記のようになります。

  • self.board には ゲーム盤を表す 2 次元の list が代入されている
  • self.key には 1 つ目の添字の値 である 0 が代入されている
  • key には 2 つ目の添字の値 である 1 が代入されている 

従って、上記の __getitem__ の返り値self.board[0][1] となり、これはゲーム盤の (0, 1) の座標のマスの値 を表します。

上記が、board[0][1] を記述した際の 処理の流れ です。

上記のような かなりわかりづらく、まわりくどい処理 を行う必要があるのは、__getitem__ メソッドが残念ながら 1 つの添字の記述に対する処理にしか対応していない ためです。

実は 2 次元の list でゲーム盤のデータを表現する場合は ListBoardwithFirstkey クラスを定義する必要はありません。その代わりに下記のプログラムのように ListBoard クラスの __getitem__ メソッドの返り値を self.board[key] を返すように定義することができます。興味がある方はそのように定義してみて下さい。

def __getitem__(self, key):
    return self.board[self.key]

上記のように定義することで、board[0][1] を記述した場合の board[0] の部分が __getitem__ メソッドの返り値である board.board[0] となるので、board[0][1] の値が board.board[0][1] となり、(0, 1) のマスを表すことになります。

ただし、この手法はゲーム盤のデータを 2 次元の list で表現した場合でしか利用できません。例えばこの後で説明する 1 次元の list でゲーム盤を表現する場合は ListBoardwithFirstkey のようなクラスを定義する必要があります。

__setitem__ メソッドの定義

次に、board[0][1] = Marubatsu.CIRCLE のような 2 つの添字 を記述した場合の 代入処理を行える ように __setitem__ メソッドを定義します。残念ながら __setitem__ メソッドも 1 つの添字の記述に対する処理にしか対応していない ので、__getitem__同様の工夫を行う必要 があります。どのように定義すればよいかについて少し考えてみて下さい。

初心者の方は ListBoard クラスの __setitem__ メソッドを定義する必要があると思ったかもしれませんが、ListBoard クラスの __setitem__ メソッドは board[1] = Marubatsu.CIRCLE のような 1 つ目の添字に対応する処理 を行うメソッドなので、board[0][1] = Marubatsu.CIRCLE のような 2 つ以上の添字 に対する 代入処理を行うことはできません

board[0][1] = Marubatsu.CIRCLE という 代入文を実行 する際には、先程の board[0][1]参照の場合と同様 に、最初に board[0] の計算 が行われます。具体的には、下記のプログラムの手順 で処理が実行されます。なお、現状では __setitem__ メソッドが定義されていないので下記のプログラムを実行するとエラーが発生するので実行はしません。

b0 = board[0]
b0[1] = Marubatsu.CIRCLE

上記のうちの、b0 = board[0] の処理 は先ほどの board[0][1] の参照 で行われる処理と 全く同じ です。そのため、ListBoard__getitem__ メソッドを 修正する必要はありません

従って、b0 には ListBoardwithFirstkey クラスのインスタンスが代入 されているので、b0[1] = Marubatsu.CIRCLE を実行すると、ListBoardwithFirstkey クラスの __setitem__ メソッドが呼び出されます。その中で 2 つの添字 で指定した マスの値を更新 するためには、下記のプログラムのように __setitem__ メソッドを定義します。

  • 1、2 行目1 つ目の添字の値 を表す self.key2 つ目の添字の値 を表す key を用いて、添字が指定する board 属性の要素に 代入文の式の値 である value を代入 する
  • 4 行目ListBoardwithFirstkey クラスのメソッドとして __setitem__ を定義する
1  def __setitem__(self, key, value):
2      self.board[self.key][key] = value
3
4  ListBoardwithFirstkey.__setitem__ = __setitem__
行番号のないプログラム
def __setitem__(self, key, value):
    self.board[self.key][key] = value

ListBoardwithFirstkey.__setitem__ = __setitem__

なお、ListBoard__setitem__ メソッドは board[1] = Marubatsu.CIRCLE のような 添字を 1 つだけ記述した代入文 を実行した際に呼び出されるので、修正する必要はありません

2 次元の list の場合は board[1] = ["o", "x", "o"] のような、ゲーム盤の特定の列を代入することができますが、ListBoard ではそのような代入処理を行うことはできません。現時点ではそのような代入処理を行う必要がないので実装しませんが、そのような代入処理が必要となる場合は ListBoard クラスの __setitem__ メソッドの中にそのような代入処理を行うプログラムを記述する必要があります。

上記の定義後に下記のプログラムを実行すると、実行結果から board[0][1] = Marubatsu.CIRCLE によって (1, 2) のマスに 〇 が配置された ことが確認できます。

board = ListBoard()
board[0][1] = Marubatsu.CIRCLE
print(board[0][1])
print(board.board)

実行結果

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

以上で ListBoard クラスの インスタンス に対して、2 次元の list で直接ゲーム盤のデータを記録した場合と 同様2 つの添字を記述 してゲーム盤のマスの 参照と代入を行う ことができるようになりました。

Marubatsu クラスの修正

上記の ListBoard を利用した ゲーム盤のデータを作成 して board 属性を記録 するように Marubatsu クラスを修正します。修正方法は 先ほど説明 した方法と ほぼ同じ ですが、ListBoard クラスのインスタンスを作成する際に、ゲーム盤の大きさを表す board_size実引数に記述 できるようにした点が 異なります

下記は __init__ メソッドを修正したプログラムです。

  • 1、3 行目:デフォルト値を ListBoard とする、ゲーム盤のデータ構造を代入する仮引数 boardclass を追加して同名の属性に代入する
1  def __init__(self, boardclass=ListBoard, board_size=3, count_linemark=False, check_coord=True):
2      # ゲーム盤のデータ構造を定義するクラス
3      self.boardclass = boardclass
4      # ゲーム盤の縦横のサイズ
5      self.BOARD_SIZE = board_size
元と同じなので省略
6  
7  Marubatsu.__init__ = __init__
行番号のないプログラム
def __init__(self, boardclass=ListBoard, board_size=3, count_linemark=False, check_coord=True):
    # ゲーム盤のデータ構造を定義するクラス
    self.boardclass = boardclass
    # ゲーム盤の縦横のサイズ
    self.BOARD_SIZE = board_size
    # 各直線上のマークの数を数えるかどうか
    self.count_linemark = count_linemark
    # move と unmove メソッドで座標などのチェックを行うかどうか
    self.check_coord = check_coord
    # 〇×ゲーム盤を再起動するメソッドを呼び出す
    self.restart()

Marubatsu.__init__ = __init__
修正箇所
-def __init__(self, board_size=3, count_linemark=False, check_coord=True):
+def __init__(self, boardclass=ListBoard, board_size=3, count_linemark=False, check_coord=True):
    # ゲーム盤のデータ構造を定義するクラス
+   self.boardclass = boardclass
    # ゲーム盤の縦横のサイズ
    self.BOARD_SIZE = board_size
元と同じなので省略

Marubatsu.__init__ = __init__

下記は initialize_board メソッドを修正したプログラムです。

  • 2 行目board 属性に boardclass 属性に代入された クラスのインスタンス を代入する。その際に、ゲーム盤のサイズを表す値を実引数に記述するようにした
1  def initialize_board(self):
2      self.board = self.boardclass(self.BOARD_SIZE)
3      
4  Marubatsu.initialize_board = initialize_board
行番号のないプログラム
def initialize_board(self):
    self.board = self.boardclass(self.BOARD_SIZE)
    
Marubatsu.initialize_board = initialize_board
修正箇所
def initialize_board(self):
-   self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+   self.board = self.boardclass(self.BOARD_SIZE)
    
Marubatsu.initialize_board = initialize_board

動作の確認

上記の修正後に、下記のプログラムで play メソッドを利用して ai2s VS ai14s の対戦を 1 回行う と、実行結果のように プログラムが正しく動作 することが確認できました。

from ai import ai2s, ai14s

mb = Marubatsu()
mb.play(ai=[ai2s, ai14s])

実行結果(対戦結果はランダムなので下記と異なる場合があります)

Turn o
...
...
...

Turn x
.O.
...
...

Turn o
.o.
.X.
...

Turn x
.oO
.x.
...

Turn o
Xoo
.x.
...

Turn x
xoo
.xO
...

winner x
xoo
.xo
..X

1 次元の list でゲーム盤を表現するクラスの定義

ゲーム盤を表す 別のデータ構造 として、1 次元の list でゲーム盤を表現するという方法が考えられます。具体的には以前の記事で紹介した 数値座標 をインデックスとした 1 次元の list でゲーム盤を表現します。下記は、そのようなデータ構造でゲーム盤を表現する List1dBoardList1dBoardwithFirstkey というクラスの定義です。定義の方法は ListBoardListBoardwithFirstkeyほぼ同様 です。なお、クラスの名前の中の 1d の d次元 を表す dimension の頭文字 です。

下記は List1dBoard クラスの定義です。先ほどの ListBoard との違いについて説明します。

  • 4 行目ゲーム盤のマスの数 は「ゲーム盤のサイズの 2 乗」なので、1 次元の listゲーム開始時のゲーム盤 を表現する場合は、マスの数だけ Marubatsu.EMPTY を要素とする list を作成すればよい。そのような list を board 属性に代入するように修正した7
  • 7 行目List1dBoardwithFirstkey__getitem____setitem__ メソッドでは、(x, y)2 次元の座標 から「x + y × ゲーム盤のサイズ」という式で 数値座標を計算 する必要がある。そのため ゲーム盤のサイズの情報が必要 となるので、その値を 2 つ目の実引数に記述 してインスタンスを作成するようにした
  • 9、10 行目:ListBoard の場合と同様に __setitem__ メソッドでは処理は行わない
 1  class List1dBoard(Board):
 2      def __init__(self, board_size=3):
 3          self.BOARD_SIZE = board_size
 4          self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
 5          
 6      def __getitem__(self, key):
 7          return List1dBoardwithFirstkey(self.board, self.BOARD_SIZE, key)
 8      
 9      def __setitem__(self, key, value):
10          pass
行番号のないプログラム
class List1dBoard(Board):
    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
        self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
        
    def __getitem__(self, key):
        return List1dBoardwithFirstkey(self.board, self.BOARD_SIZE, key)
    
    def __setitem__(self, key, value):
        pass
修正箇所
-class ListBoard(Board):
+class List1dBoard(Board):
    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
-       self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+       self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
        
    def __getitem__(self, key):
-       return ListBoardwithFirstkey(self.board, key)
+       return List1dBoardwithFirstkey(self.board, self.BOARD_SIZE, key)
    
    def __setitem__(self, key, value):
        pass

下記は List1dBoardwithFirstkey クラスの定義です。先ほどの ListBoard との違いについて説明します。

  • 2、4 行目:ゲーム盤のサイズを代入する仮引数 board_size を追加 し、BOARD_SIZE 属性に代入 するように修正した
  • 8 行目:(x, y) の 2 次元の座標 から 数値座標を計算する式x + y × ゲーム盤のサイズ なので、その式を使って 1 次元の listインデックスを計算 してその 要素の値を返す
  • 10 行目:上記と同様の方法で数値座標を計算し、その要素に値を代入する
 1  class List1dBoardwithFirstkey:
 2      def __init__(self, board, board_size, key):
 3          self.board = board
 4          self.BOARD_SIZE = board_size
 5          self.key = key
 6          
 7      def __getitem__(self, key):
 8          return self.board[self.key + key * self.BOARD_SIZE]
 9      
10      def __setitem__(self, key, value):
11          self.board[self.key + key * self.BOARD_SIZE] = value 
行番号のないプログラム
class List1dBoardwithFirstkey:
    def __init__(self, board, board_size, key):
        self.board = board
        self.BOARD_SIZE = board_size
        self.key = key
        
    def __getitem__(self, key):
        return self.board[self.key + key * self.BOARD_SIZE]
    
    def __setitem__(self, key, value):
        self.board[self.key + key * self.BOARD_SIZE] = value
修正箇所
-class ListBoardwithFirstkey:
+class List1dBoardwithFirstkey:
-   def __init__(self, board, key):
+   def __init__(self, board, board_size, key):
        self.board = board
+       self.BOARD_SIZE = board_size
        self.key = key
        
    def __getitem__(self, key):
-       return self.board[self.key][key]
+       return self.board[self.key + key * self.BOARD_SIZE]

    def __setitem__(self, key, value):
-       self.board[self.key][key] = value
+       self.board[self.key + key * self.BOARD_SIZE] = value

上記の修正後に、下記のプログラムで boardclass=List1dBoard を実引数に記述することで List1dBoard のデータ構造でゲーム盤を記録する ようにした Marubatsu クラスのインスタンスで play メソッドを利用して ai2s VS ai14s の対戦を 1 回行う と、実行結果のように プログラムが正しく動作 することが確認できました。

mb1d = Marubatsu(boardclass=List1dBoard)
mb1d.play(ai=[ai2s, ai14s])

実行結果(対戦結果はランダムなので下記と異なる場合があります)

Turn o
...
...
...

Turn x
.O.
...
...

Turn o
.o.
.X.
...

Turn x
.oO
.x.
...

Turn o
Xoo
.x.
...

Turn x
xoo
.xO
...

winner x
xoo
.xo
..X

また、下記のプログラムで ListBoardList1dBoard のそれぞれを利用する場合の ゲーム盤のデータを表す list を表示 すると、実際にゲーム盤のデータが 2 次元の list1 次元の list という 異なるデータで記録されている ことが確認できます。

print(mb.board.board)
print(mb1d.board.board)

実行結果

[['x', '.', '.'], ['o', 'x', '.'], ['o', 'o', 'x']]
['x', 'o', 'o', '.', 'x', 'o', '.', '.', 'x']

上記の ListBoard と List1dBoard の定義からわかるように、ゲーム盤に対する処理 を行うために 必要なメソッドを定義 した ゲーム盤のデータを表すクラスを定義 することで、Marubatsu クラス のプログラムを 変更することなく異なるデータ構造のゲーム盤 で Marubatsu クラスのプログラムを 動作する ことができます。このようなことが行えるのが ポリモーフィズムの大きな利点の一つ です。

なお、1 次元の list でゲーム盤のデータを表現するというデータ構造には プログラムのわかりやすさ処理速度 の点で 大きなメリットはありませんList1dBoard を実装 したのは 簡単に実装できる ため、ポリモーフィズムの利点 がわかりやすく 実感できるのではないかと思ったから です。プログラムのわかりやすさや処理速度に優れる他のデータ構造については次回の記事以降で紹介する予定です。

List1dBoard のインスタンスに対して board[0:2][1:3] のようなスライス表記で添字を記述するとエラーが発生します。その理由は、List1dBoardwithFirstkey の key 属性や、__getitem____setitem__ メソッドの仮引数 key に数値型とは異なる、組み込みクラスである slice のインスタンスが代入されるため、__getitem__ メソッドの self.board[self.key + key * self.BOARD_SIZE] の処理で slice 型と整数型という異なるデータ型の演算が行われてしまうからです。

現状ではそのような処理を行う必要がないので本記事では実装しませんが、添字にスライス表記を記述した場合の処理に対応する必要がある場合は、self.keykeyslice 型のデータが代入されているかどうかを判定し、代入されていた場合にスライス表記に対応した処理を記述する必要があります。

なお、ListBoard の場合は ListBoardwithFirstkey の __getitem____setitem__self.board[self.key][key] のように、どちらの添字に対してもスライス表記を利用できる 2 次元の list に対して添字をそのまま記述しているので、スライス表記を記述してもエラーは発生しません。

slice クラスの詳細については下記のリンク先を参照して下さい。

ListBoard の問題点

実は上記で定義した ListBoard にはいくつかの問題があります。

処理速度の低下

その一つは 処理速度が遅くなる というものです。下記は ai2s VS ai2s の対戦を ListBoard を利用 した Marubatsu クラスのインスタンスで 10000 回行うプログラムです。

from ai import ai_match

ai_match(ai=[ai2s, ai2s], match_num=5000)

実行結果

ai2s VS ai2s
100%|██████████| 5000/5000 [00:03<00:00, 1645.19it/s]
count     win    lose    draw
o        2940    1442     618
x        1475    2899     626
total    4415    4341    1244

ratio     win    lose    draw
o       58.8%   28.8%   12.4%
x       29.5%   58.0%   12.5%
total   44.1%   43.4%   12.4%

下記は 前回の記事同じプログラムを実行 した場合の実行結果です。

ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2812.18it/s]
count     win    lose    draw
o        2923    1410     667
x        1444    2942     614
total    4367    4352    1281

ratio     win    lose    draw
o       58.5%   28.2%   13.3%
x       28.9%   58.8%   12.3%
total   43.7%   43.5%   12.8%

2 つの結果を見比べると、対戦成績はほぼ同じ なので、ListBoard を利用した場合の 処理が正しく行われている ことが確認できますが、1 秒間の 対戦回数の平均 が 2812.18 回から 1645.19 回に大幅に減っている ことがわかります。両者の違いListBord を利用しているか どうかなので、明らかに ListBoard処理速度の低下の原因 であることがわかります。

プログラムが正しく動作しない場合がある

本記事を記述後に gui_play を実行して GUI での対戦を行ってみたところ、正しく動作しない ことが判明しました。また、ai_abs_dls などの ゲーム木の探索を行う AI置換表を利用する場合 でも 正しく動作しない ことが判明しました。その理由は、__getitem____setitem__ 以外にも 定義する必要があるメソッド があり、そのメソッドを 定義していない からです。そのメソッドと修正方法については次回の記事で説明します。

次回の記事では ListBoard の処理速度の改善と、定義していない必要なメソッドについて説明します。余裕がある方はその方法について考えてみて下さい。

今回の記事のまとめ

今回の記事では ポリモーフィズム について説明し、必要なメソッドを定義 した 2 次元の list1 次元の listゲーム盤を表現するクラスを定義 することで、Marubatsu クラスの プログラムを変更せず に、異なるデータ構造のゲーム盤で処理を行うことができる という ポリモーフィズムの利点 を紹介しました。ただし、今回行った実装にはいくつかの問題があることがわかりましたので、次回の記事ではその修正を行う予定です。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu.py 本記事で更新した marubatsu_new.py

次回の記事

  1. __iter__ については必要があれば今後の記事で紹介します。シーケンスについては次回の記事で説明し、__getitem__ については今回の記事で紹介します

  2. __str__ メソッドが定義されていない場合は代わりに <__main__.Mylist object at 0x00000188459C4510> のような、そのオブジェクトの作成元のクラス名が表示されます

  3. 電池のアダプターを知らない方は「電池 アダプター」で検索してみて下さい

  4. __getitem__ メソッドが定義されていない場合に添字を記述するとエラーが発生します

  5. print で list の内容を表示するためには __str__ メソッドを定義する必要があります

  6. 1 つの添字で座標を表す方法がありますが、その場合は board[x][y] とは異なる方法で (x, y) のマスを記述する必要があるため、Marubatsu クラスのプログラムを修正する必要が生じます。その方法については次回の記事で紹介します

  7. ** はべき乗を計算する演算子で、a ** 2 は $a ^ 2$ を計算します

0
0
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?