0
0

Pythonで〇×ゲームのAIを一から作成する その82 self の意味の混同に由来する様々なエラー

Last updated at Posted at 2024-05-19

目次と前回の記事

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

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

ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

クラスによる GUI の機能の分離(続き)

前回の記事では、Marubatsu_GUI クラスを定義 することで、Marubatsu クラスから GUI の機能を分離 する作業を開始しました。

前回の記事では比較的スムーズに分離の作業が行えましたが、今回の記事では、作業を行うたびに様々な問題が発生し、それを修正するという、かなり面倒な作業を何度も行います。

もちろん、プログラミングの技能が上達すれば、発生する可能性のある問題をあらかじめ考慮しながら作業を行うことで、今回の記事よりも効率よく作業を行うことができるようになりますが、そのためには実際に面倒な作業を体験することが重要だと思います。また、今回の記事のような面倒な作業は、プログラムの構造を変更するような修正を行う際に、実際に行う必要がある場合がよくあるので紹介することにします。

ゲーム盤の画像を描画するの関数の定義

前回の記事の最後で、ゲーム盤を描画する Figure を作成したので、次は ゲーム盤の画像を描画 する処理を行っていた Marubatsu クラスの draw_boardMarubatsu_GUI クラスのメソッドとして定義し直す ことにします。

set_button_statusupdate_widgets_status メソッドの定義

前回の記事で、「必要がない限り、ローカル変数やローカル関数を使わずに、属性とメソッドを利用する」という方針をとることにしたので、draw_board のローカル関数 として定義されていた set_button_statusupdate_widgets_status を、下記のプログラムのように Marubatsu_GUI のメソッドとして定義 することにします。

  • 3 ~ 7 行目set_button_status は、Marubatsu_GUI クラスの情報を利用しない ので、@staticmethod を使って 静的メソッドとして定義 する。なお、この部分を通常のメソッドとして定義しても問題はない
  • 12 ~ 17 行目update_widgets_statusMarubatsu_GUI のメソッドとして定義する
 1  from marubatsu import Marubatsu_GUI
 2
 3  @staticmethod
 4  # ボタンのウィジェットの状態を設定する
 5  def set_button_status(button, disabled):
 6      button.disabled = disabled
 7      button.style.button_color = "lightgray" if disabled else "lightgreen"
 8
 9  Marubatsu_GUI.set_button_status = set_button_status
10
11  # ウィジェットの状態を更新する        
12  def update_widgets_status(self):
13      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
14      set_button_status(self.first_button, self.move_count <= 0)
15      set_button_status(self.prev_button, self.move_count <= 0)
16      set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
17      set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
18
19  Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
from marubatsu import Marubatsu_GUI

@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
    button.disabled = disabled
    button.style.button_color = "lightgray" if disabled else "lightgreen"

Marubatsu_GUI.set_button_status = set_button_status

# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.move_count <= 0)
    set_button_status(self.prev_button, self.move_count <= 0)
    set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
    set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
from marubatsu import Marubatsu_GUI

+@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
    button.disabled = disabled
    button.style.button_color = "lightgray" if disabled else "lightgreen"

Marubatsu_GUI.set_button_status = set_button_status

# ウィジェットの状態を更新する        
-def update_widgets_status():
+def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.move_count <= 0)
    set_button_status(self.prev_button, self.move_count <= 0)
    set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
    set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status

draw_board の定義

次に、draw_boardmarubatsu_GUI のメソッドとして定義 する必要がありますが、その際に 下記の点に注意 する必要があります。

  • Marubatsu クラスの draw_boardself は、Marubatsu クラスのインスタンス である
  • Marubatsu_GUI クラスの draw_boardself は、Marubatsu_GUI クラスのインスタンス である
  • Marubatsu_GUI クラスのインスタンスの mb という属性 に、Marubatsu クラスのインスタンスが代入 されている
  • 従って、Marubatsu クラスの draw_boardMarubatsu_GUI クラスの draw_board として定義し直す 場合は、selfself.mb に修正する 必要がある

置換の機能の問題点

これまでの記事では、名前を変更する際 に、変更箇所が多くなかったので、変更する場所を自分で見つけて修正 を行っていましたが、上記の修正は 変更箇所が多い ので、変更箇所を 自分で探して変更するのは大変 なだけでなく、間違えやすい という問題があります。

一般的な文章を編集するソフトでは、draw_boardselfself.mb に修正するような、置換の作業を大量に行う際 に、置換の機能を使うのが一般的 です。VSCode にも置換を行う機能がありますが、下記のような問題があるので、この後で説明する シンボル名の変更 の機能を使ったほうが良いでしょう。

  • 置換の機能は、プログラムの文脈を考慮せず に、指定した文字列を置換の対象とする
  • そのため、置換の機能を利用する際は、置換を行う文字列を 一つ一つ検索 して探し、その文字列を 置換しても良いかどうかを確認しながら行う必要 がある

具体例を示します。下記のプログラムで、グローバル変数 i の名前を j に変更する必要が生じたと思ってください。

i = 1         # この i はグローバル変数

def f():
    i = 1     # この i はローカル変数
    print(i)  # この i はローカル変数
    
print(i)      # この i はグローバル変数

VSCode の置換の機能を使ってこの変更を行う場合は、下記のような手順で行います。

  1. 「編集」メニューの「置換」を選択するか、Ctrl + H を押すと下図のパネルが表示される

  2. 下図のように、上のテキストボックスに i を、下のテキストボックスに j を入力する。なお、「1/26件」はファイルの中に 26 か所の i が検索され、その中の 1 つ目の i が選択状態になっていることを表す

  3. 下図のようにセルの中の i が検索されて色がついて表示される

  4. 下図左の赤丸の「すべて置換」ボタンをクリックすると、このセルだけでなく、VSCode で編集中のファイルの すべての ij に置換されてしまう のでこの場合は 行ってはならない。例えば、上図のプログラムの場合は、下図右のようにローカル変数 iprint の中の i、コメントの中の i までもが j に変換されてしまうため、うまくいかない
     

  5. 下図左の赤丸の矢印のボタンをクリックすることで、i が順番に検索されて選択状態になるので、置換する必要がある i を検索状態にする。下図右は、プログラムが記述されたセルの、最初の i を選択状態(灰色で表示される)にした図である
     

  6. 下図左の赤丸の「置換」ボタンをクリックすると、下図右のように、選択状態の i だけが j に置換される
     

  7. 上記の作業を、置換する必要があるすべての i に対して行う

置換の作業は、上記の例のように、置換する必要がある i置換してはいけない i混在する ような場合は、かなり面倒 で、間違えやすい 作業が必要になります。

VSCode のシンボル名の変更の使い方

VSCode には、プログラムの文法から、関連する名前だけを置換 するという、「シンボル名の変更1という機能があります。上記のプログラムのグローバール変数 ij に変更する作業は、シンボル名の変更を使って下記の手順で 簡単に行うことができます

  1. 変更するグローバル変数 i をマウスでドラッグして 選択状態にする

  2. マウスの右ボタンを押して表示される下図のメニューから「シンボル名の変更」を選択するか、F2 キーを押す

  3. 下図のテキストボックスが表示されるので j を入力してエンターキーを押す

  4. 下図のように、グローバル変数 i のみが j に変更される

シンボル名の変更の注意点

シンボル名の変更は、プログラムの文法上 での 同じ意味の名前だけが置換 される非常に便利な機能ですが、下記の点に注意して下さい

  • コメントの内容 は、プログラムで処理を行う内容ではない ので、変更されない。実際に、上図のように、コメントの i は変更されない
  • 具体例はこの後で紹介するが、変更してはいけない名前が変更される場合がある
  • シンボル名の変更は、文法的に同じ意味を持つ名前を変更するので、下記のプログラムの a のように、文法的に正しくない名前a はグローバル変数として存在せず、ローカル変数としても定義されていない)を 変換することはできない
b = 0

def f():
    print(a)
    print(a * 2)
  • 実際に上記の a をシンボル名の変更で変更しようとすると、下図のように名前を変更できない(can`t be renamed)という意味のメッセージが表示される

上記の a の、グローバル変数 b への変更は、下記の手順で行うことができます。

  • 下記のプログラムの 4 行目のように、a = 0 を記述して a文法的に正しいローカル変数にする
1  b = 0
2
3  def f():
4      a = 0
5      print(a)
6      print(a * 2)
行番号のないプログラム
b = 0

def f():
    a = 0
    print(a)
    print(a * 2)
  • シンボル名の変更で ab に変更し、下図のようなプログラムにする
1  b = 0
2
3  def f():
4      b = 0
5      print(b)
6      print(b * 2)
  • 文法的に正しくするために追加した 4 行目のプログラムを削除する

draw_board の定義

Marubatsu_GUI のメソッドとして draw_board を定義する際に、下記の作業を行えば良いと思った人がいるかもしれません。

  • Marubatsu クラスの draw_board の定義をそのままコピーする
  • ローカル関数 set_button_statusupdate_widgets_status を削除する
  • シンボル名の変更を使って selfself.mb に変更する

しかし、上記の作業を行うと、下記のプログラムのような修正が行われ、実行すると実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、シンボル名の変更による修正箇所は自動的に行われるので、修正箇所は省略します。

def draw_board(self.mb, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board

実行結果

  Cell In[4],   line 1
    def draw_board(self.mb, ax, ai):
                       ^
SyntaxError: invalid syntax

エラーの検証と修正

エラーメッセージから、1 行目の draw_board の定義の 仮引数の部分に self.mb が記述される という、文法エラー(syntax error)が発生 していることがわかります。これは、シンボル名の変更で selfself.mb に変更した際に、変更してはいけない draw_board の仮引数 self までもが変更 されてしまったことが原因です。

上記で行ったシンボル名の変更の機能に不具合があったのではないかと思う人がいるかもしれませんが、そうではありません。シンボル名の変更 が行う処理は、文法的に同じ意味を持つ名前を変更 するという処理です。draw_board仮引数 の selfブロックの中の self は、文法的には同じ意味を持つ ので、シンボル名の変更 によって両方が self.mb に変更されるのは、正しい処理 です。

上記のような間違った変換が行われた原因は、draw_boardself の意味Marubatsu クラスと Marubatsu_GUI クラスで下記の表のように 異なるから です。

Marubatsu クラス Marubatsu_GUI クラス
仮引数の self Marubatsu のインスタンス Marubatsu_GUI のインスタンス
ブロックの中の self Marubatsu のインスタンス Marubatsu のインスタンス

上記の表のように、Marubatsu クラスの draw_board メソッド内の selfすべて Marubatsu クラスのインスタンス ですが、Marubatsu_GUI クラスの draw_board メソッド内の self は、仮引数とブロックの中で異なる意味 を持ちます。そのような違いがあるにも関わらず、Marubatsu クラスの draw_board をそのままコピーして Marubatsu_GUI クラスの draw_board として定義したため、文法上 の仮引数 self の意味と、実際 の仮引数self の意味が 食い違う という状況が発生することになります。これがエラーの原因です。

このように、あるクラスのメソッドを、別のクラスのメソッドとして定義し直す際に、異なる意味を持つ self が混在 する場合があります。そのような場合は、シンボル名の変更 によって self を変更した際に、変更してはいけない self がまでもが 変更されてしまう場合がある という点に注意して下さい。

Python では 慣習的に self を、クラスのインスタンスを代入する仮引数の名前として利用します が、そのせいで、self という名前の意味を混同することによるエラーが発生 する場合が良くあります。実際に、この後で発生するエラーの多くは self に関するものです。

なお、このような、異なるものに対して同じ名前が付けられてしまう という現象は、self 以外でも、プログラムの修正 による バグの原因 として 頻繁に発生する ので注意が必要です。

この問題は、下記のプログラムのように、draw_board の仮引数を self にすることで修正することができます。

def draw_board(self, ax, ai):
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
修正箇所
-def draw_board(self.mb, ax, ai):
+def draw_board(self, ax, ai):
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board

draw_board メソッドの処理の確認

上記で定義した draw_board メソッドが正しく動作するかどうかを、下記のプログラムで確認することにします。実行結果のように、ゲーム盤が描画されるようになりますが、同時にエラーが発生することがわかります。このエラーの原因について少し考えてみて下さい。

  • 4 行目Marubatsu_GUI のインスタンスを作成し、mb_gui に代入する
  • 5 行目Marubatsu_GUIdraw_board メソッドを呼び出して、ゲーム盤を描画する。draw_board の仮引数 axai に代入する値は、mb_gui の属性に代入されるので、それを実引数に記述する
from marubatsu import Marubatsu

mb = Marubatsu()
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

実行結果(上部のボタンなどのウィジェットの画像は省略します)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 5
      3 mb = Marubatsu()
      4 mb_gui = Marubatsu_GUI(mb, ai=[None, None])
----> 5 mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

Cell In[5], line 44
     41         color = "red" if (x, y) == self.mb.last_move else "black"
     42         self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            
---> 44 update_widgets_status()

TypeError: update_widgets_status() missing 1 required positional argument: 'self'

エラーの検証と修正

エラーメッセージから、draw_boardupdate_widgets_status を呼び出す際 に、位置引数(positional argument) self に対応する 実引数が記述されていない ことが原因であることがわかります。また、このようなエラーが発生した原因は以下の通りです。

  • Marubatsu クラスの draw_board 内では、update_widgets_status は、ローカル関数として定義 されていたので、update_widgets_status() のように記述して呼び出していた
  • Marubatsu_GUI クラスでは、先程 update_widgets_statusメソッドとして定義 したので、呼び出す際には、self.update_widgets_status() のように、Marubatsu_GUI クラスの インスタンスから呼び出す必要がある

従って、draw_board を下記のプログラムの 3 行目のように修正することで、この問題を解決することができます。

def draw_board(self, ax, ai):
元と同じなので省略      
    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
プログラム全体
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略      
-   update_widgets_status()   
+   self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board

上記の修正後に、下記のプログラムを実行すると、実行結果のように、別のエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、表示されるボタンや画像は先ほどと同じなので省略します。

mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

実行結果

Cell In[1], line 14
     12 def update_widgets_status(self):
     13     # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
---> 14     set_button_status(self.first_button, self.move_count <= 0)
     15     set_button_status(self.prev_button, self.move_count <= 0)
     16     set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)

AttributeError: 'Marubatsu_GUI' object has no attribute 'move_count'

新たなエラーの検証と修正

エラーメッセージから、update_widgets_status メソッド内で、self.move_count の処理を行う際に、Marubatsu_GUI クラスのインスタンスである selfmove_count という属性が存在していない ことが原因であることがわかります。

Marubatsu クラスの draw_board メソッド内で ローカル関数 として update_widgets_status を定義 していた際には、selfMarubatsu クラスのインスタンス であったので、self.move_count はエラーにはなりませんでした。

一方、Marubatsu_GUI クラスの メソッド として update_widgets_status を定義 した の場合は、selfMarubatsu_GUI のインスタンス を表します。

このように、update_widgets_status 内の self の意味が異なる にも関わらず、draw_board メソッドの ローカル関数そのままコピーして update_widgets_status を定義 してしまったことがこの問題の原因です。

このエラーの原因も、先程と同様に、self の意味が異なる点です。

ところで、self の意味が変化したにもかかわらず、update_widgets_status 内の self.first_button の部分でエラーが発生しない点が気になった人はいないでしょうか?Marubatsu_GUI クラスを作成する前は、< ボタンのウィジェットは play メソッド内の下記のプログラムで、Marubatsu クラスの属性に代入 していました。そのため、selfMarubatsu クラスのインスタンス が代入されたローカル関数の update_widgets_status 内では self.first_button< ボタンのウィジェットが代入 されています。

self.first_button = create_button("<<", 100)

一方、Marubatsu_GUI クラスでは、< ボタンのウィジェットは create_widgets メソッド内の上記のプログラムで、Marubatsu_GUI クラスの属性に代入 しています。そのため、selfMarubatsu_GUI クラスのインスタンス が代入された update_widgets_status メソッド内でも self.first_button< ボタンのウィジェットが代入 されています。これが、self.first_button のほうではエラーが発生しない理由です。

上記をまとめると、< ボタンのウィジェットの 代入先Marubatsu クラスの属性から、Marubatsu_GUI クラスの 属性に変更 したため、update_widgets_statusself の意味が変わっても self.first_button に < ボタンのウィジェットが代入されるということです。

非常にわかりづらいと思いますので、Marubatsu クラスと Marubatsu_GUI クラスでの、update_widgets_status の性質の違いを表にまとめます。

Marubatsu クラス Marubatsu_GUI クラス
定義 draw_board のローカル関数として定義 メソッドとして定義
self の意味 Marubatsu クラスのインスタンス Marubatsu_GUI クラスのインスタンス
ボタンの代入先 Marubatsu クラスの属性 Marubatsu_GUI クラスの属性
move_count の代入先 Marubatsu クラスの属性 Marubatsu クラスの属性

上記の表からわかるように、Marubatsu_GUI クラスの update_widgets_status メソッド内では、ボタンの代入先と、move_count などの代入先は、異なるクラスの属性に代入 されています。それにも関わらず、どちらも self.first_buttonself.move_count のように、Marubatsu_GUI クラスの属性としてそれらを記述 している点がエラーの原因です。

従って、この問題は下記のプログラムのように、update_widgets_status を修正することで解決することができます。

  • 4 ~ 7 行目Marubatsu クラスの属性を参照する必要がある move_countboard_records の先頭を self. から self.mb. に修正する
1  # ウィジェットの状態を更新する        
2  def update_widgets_status(self):
3      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
4      set_button_status(self.first_button, self.mb.move_count <= 0)
5      set_button_status(self.prev_button, self.mb.move_count <= 0)
6      set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
7      set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
8
9  Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    set_button_status(self.first_button, self.mb.move_count <= 0)
    set_button_status(self.prev_button, self.mb.move_count <= 0)
    set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
    set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
# ウィジェットの状態を更新する        
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
-   set_button_status(self.first_button, self.move_count <= 0)
+   set_button_status(self.first_button, self.mb.move_count <= 0)
-   set_button_status(self.prev_button, self.move_count <= 0)
+   set_button_status(self.prev_button, self.mb.move_count <= 0)
-   set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
+   set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
-   set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
+   set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)

Marubatsu_GUI.update_widgets_status = update_widgets_status

上記の修正後に、下記のプログラムを実行するとエラーが発生しなくなり、実行結果のようにリプレイ機能に関するボタンが灰色で表示されて操作できなくなることが確認できます。

mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

実行結果(下図は、画像なので操作することはできません)

draw_board メソッドの改良

draw_board メソッドを呼び出す際に、下記のプログラムのように、実引数に mb_gui.axmb_gui.ai を記述 していますが、draw_board の仮引数 selfmb_gui が代入される ので、mb_gui.axmb_gui.ai の値は、draw_board の中で self.axself.ai と記述して参照することができます。そのため、draw_board の仮引数 axai は必要がありません

mb_gui.draw_board(mb_gui.ax, mb_gui.ai)

そこで、draw_board から 仮引数 axai を削除する ことにします。その際に、draw_board 内の axaiself.axself.ai に修正 する必要がありますが、仮引数 axai を削除した後シンボル名の変更axself.ax に変更 しようとしても、下図のような表示が行われて 変更することができません

これは、ax を仮引数から削除 したことによって、axローカル変数として定義されていないことになる ためです。この問題を解決する方法の一つは、下記のプログラムの 2、3 行目ように、axai にそれぞれ self.axself.ai を代入する処理を記述 するというものです。このように修正することで、削除した 仮引数 axai と同じ名前のローカル変数にそれぞれの値が代入 されるので、以後のプログラムを変更する必要が無くなります。

1  def draw_board(self):
2      ax = self.ax
3      ai = self.ai
4    
5     # Axes の内容をクリアして、これまでの描画内容を削除する
6      ax.clear()
元と同じなので省略
7    
8  Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self):
    ax = self.ax
    ai = self.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
修正箇所
-def draw_board(self, ax, ai):
+def draw_board(self):
+   ax = self.ax
+   ai = self.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board

先程のノートで説明したように、上記の修正を行った後で、シンボル名の変更を使って axaiself.axself.ai に変更し、その後で、2、3 行目の代入文を削除するという方法で、axaiself.axself.ai に変更することができます。

draw_board の処理の中で、aiax に何らかの値を代入して変更する処理を行う必要がある場合は、axself.ax の中身が別々のものになるため axaiself.axself.ai に変更する必要がありますが、実際にはそのような処理は行われないので、わざわざそのような作業を行うメリットはあまりないでしょう。

実行結果は省略しますが、上記の修正後に、下記のプログラムを実行して正しい処理が行われることを確認して下さい。

  • 2 行目draw_board の実引数の記述を削除する
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board()
修正箇所
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
-mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
+mb_gui.draw_board()

以上が、Marubatsu_GUI クラスでの draw_board メソッドの定義です。self に関するバグは初心者にはわかりづらいかもしれませんが、よくあるバグの原因の一つです。

self を記述する場合は、self が具体的に何を意味しているかを意識しながらプログラムを記述することを心掛けて下さい

play メソッドの修正

まだイベントハンドラに関する処理を記述していませんが、GUI に関する描画をできるようになったので、play メソッドの中で Marubatsu_GUI を利用する ように修正します。

下記は、そのように play メソッドを修正したプログラムです。GUI に関する処理は、Marubatsu_GUI の中ですべて行うことにしたので 非常にシンプルなプログラムになります

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play

上記の修正後に、下記のプログラムで、gui_play を実行すると、ボタンとゲーム盤を描画するための Figure が表示されますが、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。

from util import gui_play

gui_play()

実行結果(下図は、画像なので操作することはできません)

略
Cell In[14], line 11
      8     Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
     10 self.restart()
---> 11 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

NameError: name 'ax' is not defined

エラーの検証と修正

エラーメッセージから、play_loop を呼び出す際に 実引数に記述した ax が定義されていない ことがわかります。ax は、Marubatsu_GUI クラスのインスタンスの 属性に代入 されているので、下記のプログラムの 3 行目のように、Marubatsu_GUI のインスタンスを mb_gui に代入し、play_loop の実引数 にキーワード引数 ax=mb_gui.ax を記述 することでこの問題を解決することができます。

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
-   return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play

上記の修正後に、下記のプログラムで gui_play を実行すると、実行結果のような別のエラーが発生します。このエラーの原因について少し考えてみて下さい。

gui_play()

実行結果

略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:682, in Marubatsu.draw_board.<locals>.update_widgets_status()
    680 def update_widgets_status():
    681     # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
--> 682     set_button_status(self.first_button, self.move_count <= 0)
    683     set_button_status(self.prev_button, self.move_count <= 0)
    684     set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)

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

新たなエラーの検証と修正

エラーメッセージから、update_widgets_status 内でエラーが発生 していることがわかりますが、よく見るとエラーが発生している下記の行の内容が、先程定義した Marubatsu_GUI のメソッドとは異なる ことがわかります。

# エラーメッセージ内のエラーが発生している行
set_button_status(self.first_button, self.move_count <= 0)

# Marubatsu_GUI クラスの update_widgets_status の該当する行の記述
set_button_status(self.first_button, self.mb.move_count <= 0)

また、エラーメッセージの in Marubatsu.draw_board.<locals>.update_widgets_status() から、エラーが発生した update_widgets_statusMarubatsu クラスの draw_board 内で定義されたローカル関数 であることがわかります。従って、このエラーは、play_loop の中 で、古い Marubatsu クラスの draw_board を呼び出している ことが原因なので、新しい Marubatsu_GUIdraw_board メソッドを呼び出す ように修正することで解決できます。

play_loop 内で draw_board は下記のプログラムのように記述して呼び出されています。

self.draw_board(ax, ai)

この selfMarubatsu クラスのインスタンス なので、Marubatsu_GUIdraw_board を呼び出すように修正するためには、Marubatsu_GUI クラスのインスタンスの情報が必要 になりますが、その情報は現状では play_loop メソッド内では利用できません。そこで、下記のプログラムの 4 行目のように、play メソッド内で、Marubatsu_GUI のインスタンスを作成した際 に、Marubatsu クラスの mb_gui という属性に代入 することにします。

  • 4、7 行目mb_guiself.mb_gui に修正する
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
5        
6      self.restart()
7      return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
8
9  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
-       Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
+       self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
-   return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play

上記のように修正することで、下記のプログラムのように、play_loop 内で Marubatsu_GUI クラスの draw_board メソッドを利用できるようになります。

  • 4、6 行目Marubatsu_GUI クラスの draw_board メソッドを呼び出すように修正する
1  def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
2                  # AI どうしの対戦の場合は画面を描画しない
3                  if ai[0] is None or ai[1] is None:
4                      self.mb_gui.draw_board()
元と同じなので省略
5          if gui:
6              self.mb_gui.draw_board()
元と同じなので省略
7
8  Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, ai, ax, params, verbose, gui):
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
-                   self.draw_board(ax, ai)
+                   self.mb_gui.draw_board()
元と同じなので省略
        if gui:
-           self.draw_board(ax, ai)
+           self.mb_gui.draw_board()
元と同じなので省略

Marubatsu.play_loop = play_loop

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行して正しい処理が行われることを確認して下さい。

gui_play()

必要のない仮引数の削除

VSCode で play_loop の定義を見ると下図のように 仮引数 ax が薄い色で表示 されます。

VSCode では、「定義されているが、その後一度も利用されない名前」がこのように 薄い色で表示 されます。そのような名前はエラーではありませんが、その名前はその後で使われていないので、必要がない可能性が高い ことを意味します。実際に ax は、先程の修正で self.draw_board(ax, ai)self.mb_gui.draw_board() に修正した結果、draw_board のブロックの内で一度も利用されなくなる ので、削除することができます。そこで下記のプログラムの 1 行目のように、play_loop から 仮引数 ax を削除することにします。

def play_loop(self, ai, params, verbose, gui):
元と同じなので省略

Marubatsu.play_loop = play_loop
プログラム全体
def play_loop(self, ai, params, verbose, gui):
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, ax, params, verbose, gui):
+def play_loop(self, ai, params, verbose, gui):
元と同じなので省略

Marubatsu.play_loop = play_loop

play_loop の仮引数を修正したので、play メソッド内で play_loop を呼び出す処理を下記のプログラムの 3 行目のように修正する必要があります。

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
    return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
-   return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行して正しい処理が行われることを確認して下さい。

gui_play()

他の play_loop の仮引数の削除

以下のように思った人はいないでしょうか?

  • aiMarubatsu_GUI の属性に代入されるので、play_loop の仮引数 ai がなくても play_loop のブロックの中では self.mb_gui.ai と記述することで参照できる
  • 同様の理由で play_loop の仮引数 params がなくても self.mb_gui.params と記述することで参照できる
  • 従って、ax と同様に、aiparamsplay_loop の仮引数から削除できる

しかし、残念ながら下記の理由から、aiparamsplay_loop の仮引数から削除することはできません。

  • Marubatsu_GUI のインスタンス は、play メソッドの中で、GUI で〇×ゲームを遊ぶ場合でしか作られない
  • CUI でゲームを遊ぶ場合 でも play_loop は呼び出されるが、その場合は self.mb_gui には何も代入されていない ので利用できない

しかし、別の方法で play_loop の仮引数を削除することができます。現状では、play_loop には aiparamsverbosegui の 4 つの仮引数がありますが、これらを Marubatsu_GUI ではなく、Marubatsu クラスのインスタンスの属性に代入 することで、play_loop の仮引数を削除 することができます。そこで、そのように play メソッドを修正することにします。

  • 3 ~ 6 行目play_loop の実引数に記述するデータを Marubatsu クラスのインスタンスの属性に代入する
  • 7 行目play_loop の実引数を削除する
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
2      # 一部の仮引数をインスタンスの属性に代入する
3      self.ai = ai
4      self.params = params
5      self.verbose = verbose
6      self.gui = gui
元と同じなので省略    
7      return self.play_loop()
8
9  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)    
        
    self.restart()
    return self.play_loop()

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # 一部の仮引数をインスタンスの属性に代入する
+   self.ai = ai
+   self.params = params
+   self.verbose = verbose
+   self.gui = gui
元と同じなので省略    
-   return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)
+   return self.play_loop()

Marubatsu.play = play

次に、play_loop を下記のプログラムのように修正します。

  • 1 行目self 以外の仮引数を削除する
  • 2 ~ 5 行目:削除した仮引数と同じ名前のローカル変数に、対応する属性を代入する
1  def play_loop(self):
2      ai = self.ai
3      params = self.params
4      verbose = self.verbose
5      gui = self.gui
元と同じなので省略
6
7  Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self):
    ai = self.ai
    params = self.params
    verbose = self.verbose
    gui = self.gui
    
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.mb_gui.draw_board()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, params, verbose, gui):
+def play_loop(self):
+   ai = self.ai
+   params = self.params
+   verbose = self.verbose
+   gui = self.gui
元と同じなので省略

Marubatsu.play_loop = play_loop

シンボル名の変更を使って ai などを ai.self に変更することは可能ですが、面倒なだけで大きなメリットはないので本記事では採用しないことにします。

重複する属性の削除

上記で、Marubatsu クラスのインスタンスに aiparamsverbosegui の 4 つの属性を追加しましたが、上記の中の aiparams の値は、Marubatsu_GUI クラスの __init__ メソッドの中で、下記のプログラムのように Marubatsu_GUI クラスのインスタンスの属性にも代入しています。

def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]

    self.mb = mb
    self.ai = ai
    self.ai_dict = ai_dict
    self.ai_params = params
    self.size = size
以下略

同じデータMarubatsuMarubatsu_GUI の両方のインスタンスに 重複して代入するのは無駄 なので、aiparamsMarubatsu クラスの インスタンスの属性のみに代入する ことにします。まず、Marubatsu_GUI クラスの __init__ メソッドを下記のプログラムのように修正します。なお、ai_dictsize は、GUI で〇×ゲームを遊ぶ際でのみ必要となる データなので、これまで通り Marubatsu_GUI の属性に代入する ことにします。

  • 1 行目:仮引数 aiparams を削除する
  • aiparams に関する処理を削除する
 1  def __init__(self, mb, ai_dict=None, size=3):
 2      # ai_dict が None の場合は、空の list で置き換える
 3      if ai_dict is None:
 4          ai_dict = {}
 5
 6      self.mb = mb
 7      self.ai_dict = ai_dict
 8      self.size = size
元と同じなので省略
 9
10  Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}

    self.mb = mb
    self.ai_dict = ai_dict
    self.size = size
    
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
        
    self.create_widgets()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__
修正箇所
-def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
+def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
-   if params is None:
-       params = [{}, {}]

    self.mb = mb
-   self.ai = ai
    self.ai_dict = ai_dict
-   self.ai_params = params
    self.size = size
元と同じなので省略

Marubatsu_GUI.__init__ = __init__

次に、play メソッドを下記のプログラムのように修正します。

  • 3、4 行目__init__ メソッド内に記述されていた、paramsNone の場合の処理を記述する2
  • 10 行目Marubatsu_GUI の実引数から aiparams を削除する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
 2      # params が None の場合のデフォルト値を設定する
 3      if params is None:
 4          params = [{}, {}]
 5       
 6      # 一部の仮引数をインスタンスの属性に代入する
 7      self.ai = ai
元と同じなので省略
 8      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
 9      if gui:
10          self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
元と同じなので省略
11
12  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
        
    self.restart()
    return self.play_loop()

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
+   if params is None:
+       params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
-       self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)     
+       self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
元と同じなので省略

Marubatsu.play = play

上記の修正後に、下記のプログラムで、gui_play を実行すると、実行結果のようなエラーが発生します。エラーの原因について少し考えてみて下さい。

gui_play()

実行結果

略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:770, in Marubatsu_GUI.create_dropdown(self)
    767 # ai に代入されている内容を ai_dict に追加する
    768 for i in range(2):
    769     # ラベルと項目の値を計算する
--> 770     if self.ai[i] is None:
    771         label = "人間"
    772         value = "人間"

AttributeError: 'Marubatsu_GUI' object has no attribute 'ai'

エラーの検証と修正

エラーメッセージから、create_dropdown 内の self.ai[i] で、Marubatsu_GUI クラスのインスタンスに ai が存在しないことが原因であることが分かります。これは、先程 ai のデータを Marubatsu_GUI クラスの属性に代入しないようにした ことが原因です。

従って、このエラーは、Marubatsu_GUI の中で ai 属性を利用する処理を、self.ai から self.mb.ai に修正することで解決できます。

まず、create_dropwodn メソッドを下記のプログラムのように修正します。

  • 7、11、12 行目self.aiself.mb.ai に修正します
 1  import ipywidgets as widgets
 2
 3  def create_dropdown(self):
元と同じなので省略
 4      # ai に代入されている内容を ai_dict に追加する
 5      for i in range(2):
 6          # ラベルと項目の値を計算する
 7          if self.mb.ai[i] is None:
 8              label = "人間"
 9              value = "人間"
10          else:
11              label = self.mb.ai[i].__name__        
12              value = self.mb.ai[i]
元と同じなので省略
13
14  Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
import ipywidgets as widgets

def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = self.mb.ai[i].__name__        
            value = self.mb.ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )      
        
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
import ipywidgets as widgets

def create_dropdown(self):
元と同じなので省略
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
-       if self.ai[i] is None:
+       if self.mb.ai[i] is None:
            label = "人間"
            value = "人間"
        else:
-           label = self.ai[i].__name__        
+           label = self.mb.ai[i].__name__        
-           value = self.ai[i]
+           value = self.mb.ai[i]
元と同じなので省略

Marubatsu_GUI.create_dropdown = create_dropdown

aidraw_board メソッドでも利用されているので、draw_board メソッドを下記のプログラムの 3 行目のように修正する必要があります。

def draw_board(self):
    ax = self.ax
    ai = self.mb.ai
元と同じなので省略    
    
Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self):
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
修正箇所
def draw_board(self):
    ax = self.ax
-   ai = self.ai
+   ai = self.mb.ai
元と同じなので省略    
    
Marubatsu_GUI.draw_board = draw_board

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行して正しい処理が行われることを確認して下さい。

gui_play()

今回の記事のまとめ

今回の記事では、Marubatsu_GUI クラスを定義 することで、Marubatsu クラスから GUI の機能を分離 する作業の中で、ゲーム盤の描画を行う処理を実装しました。

その際に、同じ self が異なる意味で使われることに由来する多くのエラー を紹介し、エラーの修正を行いました。

self に限らず、名前の混同に関するエラー はよくあるエラーなので原因と対処方法についてしっかりと理解しておくことを強くお勧めします。

なお、現状ではまだイベントハンドラの機能を Marubatsu_GUI に記述していないので、ゲームを遊ぶことはできません。また、現状のプログラムにはまだいくつかの問題があります。イベントハンドラの機能の実装と問題の修正は次回の記事で行います。

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

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

以下のリンクは、今回の記事で更新した marubatsu.py です。なお、Marubatsu クラスの draw_board はもう必要がなくなったので削除しました。

次回の記事

  1. シンボル名とは、変数や関数などの名前の事を表します

  2. 実は、前回の記事でこの処理を __init__ メソッドに移動したのは間違いでした。その理由は、paramsNone が代入されていた場合は、Marubatsu_GUIparams 属性には [{}, {}] が代入されますが、play メソッドのローカル変数の中身は None のままになってしまうためです

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