LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その23 勝敗判定

Last updated at Posted at 2023-10-29

目次と前回の記事

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

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

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

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

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

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

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

前回までのおさらい

前回の記事では仕様 4、5 の手番と着手を実装しました。

勝敗判定

1 ~ 5 までの仕様の実装が完了しましたので、次は 勝敗判定 に関する 6、7 の実装を行います。仕様 6 は 勝利条件、仕様 7 は 引き分けの条件 についての仕様ですが、先に仕様 6 の勝利条件の実装を行います。

勝敗判定を行うメソッドの設計

マークを配置する処理を place_mark、着手を行う処理を move というメソッドで実装したのと同様に、勝敗判定を行う メソッドを定義 することにします。そのためには、メソッドの 名前入力処理出力 を決める必要があります。

判定は英語で judgement、decision のように表現します。ゲームの勝敗判定の場合は judgement のほうが適切な気がしますので、メソッドの名前 は判定の 動詞 である judge にすることにします。

勝敗判定は、その時点での ゲーム盤の状態 に対して行い、ゲーム盤の状態はインスタンスの board 属性 に記録されているので、judge メソッドにはインスタンスを表す self 以外の引数は 必要ありません

勝敗判定が行う 処理 は、以下の 4 つの ゲームの状態 を判定することです。

  • 〇 のプレイヤーの勝利
  • × のプレイヤーの勝利
  • 引き分け
  • ゲームの決着がまだついていない

judge メソッドの 出力 である 返り値 は、judge メソッドの処理によって判定された ゲームの状態 です。ゲームの状態を返り値として返すためには、上記の 4 つの ゲームの状態 を表す データ構造を決める 必要があります。

勝敗判定の結果を表すデータ構造

前回の記事 で説明したように、同じデータ に対して、様々な データ構造が考えられます。今回の記事では勝敗判定の結果を表す 2 種類のデータ構造を紹介します。

2 種類のデータから構成されるデータ構造

最初に、2 種類のデータ から構成されるデータ構造を紹介します。

judge メソッドが判定する下記の 4 つのゲームの状態のうち、上の 3 つは 勝者(本記事では 引き分け を勝者に 含む ことにします)を、最後の 1 つは ゲームが続行中かどうか を表すデータです。

  • 〇 プレイヤーの勝利
  • × のプレイヤーの勝利
  • 引き分け
  • ゲームの決着がまだついていない

上記のように、ゲームの状態は 勝者ゲームが続行中かどうか を表す 2 種類の状態 から 構成される ので、その 2 種類 のデータで表すというデータ構造が考えられます。

勝者を表すデータ

勝者には、〇 のプレイヤー× のプレイヤー引き分け3 種類 があります。〇 のプレイヤーと、× のプレイヤーを表すデータに関しては、ゲーム盤に配置したマークや、手番と同様に Marubatsu クラスの属性 である、Marubatsu.CIRCLEMarubatsu.CROSSそのまま利用 することが出来ます。以前の記事でも説明しましたが、同じ意味を表すデータ は、共通のデータを利用 したほうがプログラムがわかりやすくなります。

これまでに記述してきたプログラムの中に、引き分け を表すデータは 存在しない ので、ここで引き分けを表すデータを 決める 必要があります。引き分けを表すデータは、〇 のプレイヤーと × のプレイヤーを表すデータと 区別できれば良い ので、そのデータは、Marubatsu.CIRCLEMarubatsu.CROSS 以外のデータ であれば 何でも構いません。そこで、本記事では、"draw" という文字列 を引き分けを表すデータにすることにし、Marubatsu クラスの DRAW 属性 にその値を代入し、引き分けを表すデータを Marubatsu.DRAW のように記述することにします。

下記のプログラムは、marubatsu モジュールに記述した Marubatsu クラスをインポートし、Marubatsu クラスの DRAW 属性に "draw" という文字列を代入しています。

from marubatsu import Marubatsu

Marubatsu.DRAW = "draw"

ゲームが続行中であるかどうかを表すデータ

ゲームが続行中であるかどうかを表すデータは、続行中である続行中でない の 2 種類です。~である~でない のような 2 種類 のデータは、論理型TrueFalse で表現するの 一般的 なので、本記事でもそのように表現することにします。

judge メソッドの実装例

初心者の方にとっては、judge メソッドの中で、この 2 種類のデータをどのように記述するかがわからない人が多いと思いますので、judge メソッドの実装例を紹介します。

プログラムでは、データを変数に代入 して扱うのが一般的なので、この 2 つのデータを judge メソッドの中で代入する ローカル変数の名前 を決めることにします。

英語で 勝者 のことを winnerゲームを行っている ことを playing と表現するので、勝者 を表すローカル変数の 名前winnerゲームが続行中であること を表すローカル変数の 名前playing と名付けることにします。

下記のプログラムは judge メソッドを、必ず 〇 が勝利する という判定を行うように実装しています。2 行でローカル変数 winnerMarubatsu.CIRCLE を代入 することで 〇が勝利した ことを、3 行目でローカル変数 playingFalse を代入 することで、ゲームが終了したこと を表現しています。

2 つ以上のデータ を関数の 返り値として返す 場合は、tuple が良く使われます1。4 行目の return 文では、この 2 つの値を要素とする tuple を使って、ゲームの状態を表す 2 つのデータを返り値として返しています。

以前の記事 で説明したように、tuple の () は省略することが出来ます。下記のプログラムの 4 行目は return (winner, playing)() を省略しています。

def judge(self):
    winner = Marubatsu.CIRCLE
    playing = False
    return winner, playing

Marubatsu.judge = judge

もちろん、上記の judge メソッドは 正しい勝敗判定を行っていません。正しい勝敗判定の実装方法については今回の記事のしばらく後で説明します。

下記のプログラムは、1 行目で Marubastu のインスタンスを作成し、2 行目で tuple を返り値として返す judge メソッドを呼び出しています。以前の記事 で説明したように、2 行目のように記述することで、tuple の要素別々の変数に代入 する処理を 1 行の文で記述 することが出来ます。具体的には、2 行目では、グローバル変数 winnerplaying に、judge メソッドの返り値である tuple の 2 つの要素をそれぞれ代入しています。その結果、実行結果に 〇 のプレイヤーが勝者であることを表す "o" と、ゲームが終了したことを表す False が表示されます。

先程のノートで説明したのと同様に、2 行目の winner, playing = の部分は、(winner, playing) =() を省略 しています。

mb = Marubatsu()
winner, playing = mb.judge()
print(winner)
print(playing)

実行結果

o
False

1 種類のデータから構成されるデータ構造

次に、1 種類のデータ から構成されるデータ構造を紹介します。

勝敗判定によって、判定される下記の 4 つのゲームの状態は、同時に発生 することは ありません。このゲームの 勝者 は、〇 のプレイヤー、× のプレイヤー、引き分けの いずれか であり、例えば 〇 のプレイヤーと × のプレイヤーが同時に勝利することはありません。また、勝者決まっていれば 必ず ゲームの決着がついています し、勝者が 決まっていなければ ゲームの 決着はついていません

  • 〇 プレイヤーの勝利
  • × のプレイヤーの勝利
  • 引き分け
  • ゲームの決着がまだついていない

つまり、勝敗判定 によって、上記の 4 つの状態のいずれか 1 つの状態が決まる ということです。従って、状態を表すデータ1 つのデータで表現 することが出来ます。

4 つの状態のうち、3 つは 勝者を表す状態 なので、judge メソッドの中で、勝敗判定を表すデータを代入する ローカル変数の名前 は、winner にすることにします。また、〇 のプレイヤーの勝利、× のプレイヤーの勝利、引き分けを表すデータは、先程と同様Marubatsu.CIRCLEMarubatsu.CROSSMarubatsu.DRAW で表現することします。

ゲームの決着がまだついていない状態は、ゲームの勝者が 存在しない という状態です。Python には データが存在しないこと を表す None というデータがあるので、ゲームの 決着がついていない状態 を表すデータは、None で表現 するすることにします。

これで、1 つのデータ勝敗判定の結果 を表す状態を 表現する ことが出来ます。下記の表に 4 つの状態に対応するデータを示します。

状態 データ
〇 の勝利 Marubatsu.CIRCLE
× の勝利 Marubatsu.CROSS
引き分け Marubatsu.DRAW
決着がついていない None

勝敗判定の結果を、2 つのデータと、1 つのデータの どちらのデータ構造で表現 しても、問題なく勝敗判定を行う judge メソッドを定義することが出来ます。本記事では より簡潔に プログラムを 記述できる、1 つのデータで表現する方法を採用することにします。

〇 の勝利の判定

4 つの状態を 一度で判定 することは できない ので、順番に判定する必要 があります。そこで、最初に 〇 のプレイヤーが勝利しているかどうかを判定するプログラムを実装することにします。

下記の仕様 6 から、縦、横、斜めのいずれかの 一直線の 3 マス に 〇 のマークが 並んだ場合 に 〇 のプレイヤーの勝ちとなります。

  1. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する

そのような判定を行うプログラムは、様々な方法 で記述することが出来ます。今回の記事ではその中の 1 つの方法を紹介し、他の方法については次回以降の記事で紹介することにします。

〇 の勝利条件の列挙

仕様 6 から、〇 が勝利する条件を列挙すると、下図のように、横方向に 3 種類、縦方向に 3 種類、斜め方向に 2 種類の 8 種類 があることがわかります。

〇 のプレイヤーが勝利する条件は、ゲーム盤の状態 が上図の 8 つの いずれかの状態 になる場合です。そこで、最初にゲーム盤が上図の 左上の状態 になっていることを判定するプログラムを記述することにします。

0 行の 〇 の勝利の判定の実装

先程の図の左上の状態では、ゲーム盤の 0 行 の (0, 0)、(1, 0)、(2, 0) の 3 つのマス〇 が配置 されています。このような、3 つの条件を同時に満たすことを判定するプログラムは、下記のように、if 文を入れ子 して記述することが出来ます。

def judge(self):
    # (0, 0) が 〇 であることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE:
        # さらに、(1, 0) が 〇 であることを判定する
        if self.board[1][0] == Marubatsu.CIRCLE:
            # さらに、(2, 0) が 〇 であることを判定する
            if self.board[2][0] == Marubatsu.CIRCLE:
                # 〇 が勝利する
                winner = Marubatsu.CIRCLE

    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

実装の確認

下記は、上記の judge メソッドが正しく動作しているかどうかを確認するために、ゲームの開始直後judge メソッドを呼び出して勝敗判定を行った場合と、place_mark(0, 0)、(1, 0)、(2, 0) のマスに 〇 を配置 してから judge メソッドを呼び出して勝敗判定を行った場合に、judge メソッドの返り値を表示するプログラムです。

本当は、実際に〇×ゲームを遊ぶ場合 と同様に、move メソッドを使って 〇 と × を交互に配置した状況で 確認すべき ですが、上記の if 文の動作を確認するだけ であれば、× を配置する必要がない ので、今回は place_mark メソッドを使って 〇 だけを配置 した 簡略化した確認 を行っています。move メソッドを使ったより本格的な確認については、次回の記事で紹介します。

下記のプログラムを実行すると、実行結果のようなエラーが発生します。

 1  mb = Marubatsu()
 2
 3  # ゲーム開始直後に judge メソッドを呼び出して表示する
 4  print(mb.judge())
 5
 6  # 0 列に 〇 を配置してから judge メソッドを呼び出して表示する
 7  mb.place_mark(0, 0, Marubatsu.CIRCLE)
 8  mb.place_mark(1, 0, Marubatsu.CIRCLE)
 9  mb.place_mark(2, 0, Marubatsu.CIRCLE)
10  print(mb.judge())
行番号のないプログラム
mb = Marubatsu()

# ゲーム開始直後に judge メソッドを呼び出して表示する
print(mb.judge())

# 0 列に 〇 を配置してから judge メソッドを呼び出して表示する
mb.place_mark(0, 0, Marubatsu.CIRCLE)
mb.place_mark(1, 0, Marubatsu.CIRCLE)
mb.place_mark(2, 0, Marubatsu.CIRCLE)
print(mb.judge())

実行結果

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\023\marubatsu.ipynb セル 5 line 4
      1 mb = Marubatsu()
      3 # ゲーム開始直後に judge メソッドを呼び出して表示する
----> 4 print(mb.judge())
      6 # 0 列に 〇 を配置してから judge メソッドを呼び出して表示する
      7 mb.place_mark(0, 0, Marubatsu.CIRCLE)

c:\Users\ys\ai\marubatsu\023\marubatsu.ipynb セル 5 line 1
      9             winner = Marubatsu.CIRCLE
     11 # winner を返り値として返す
---> 12 return winner

UnboundLocalError: cannot access local variable 'winner' where it is not associated with a value

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

  • UnboundLocalError
    ローカル(Local)変数が名前空間に登録されていない(unbound2)ことを表すエラー
  • local variable 'winner' referenced before assignment
    winner というローカル(local)変数(variable)が、値を代入(assignment)する前(before)に参照された(referenced)

エラーの原因

実行結果からわかるように、judge メソッドのブロックの中の 12 行目の return winner で、winner という ローカル変数値を代入する前に参照 されたという意味の エラーが発生 しています。このエラーの原因を見つけるために、プログラムで行われる処理を、順を追って確認 することにします。

まず、1 行目で mbMarubatsu クラスから作成したインスタンスを代入しています。この処理はこれまで何度も行ってきたのでエラーの原因である可能性は低いでしょう。

次に、4 行目で先ほど実装した judge メソッドを呼び出しています。下記は、judge メソッドの定義を再掲したものです。

 1  def judge(self):
 2     # (0, 0) が 〇 であることを判定する
 3     if self.board[0][0] == Marubatsu.CIRCLE:
 4         # さらに、(1, 0) が 〇 であることを判定する
 5         if self.board[1][0] == Marubatsu.CIRCLE:
 6             # さらに、(2, 0) が 〇 であることを判定する
 7             if self.board[2][0] == Marubatsu.CIRCLE:
 8                 # 〇 が勝利する
 9                 winner = Marubatsu.CIRCLE
10
11      # winner を返り値として返す
12      return winner

judge メソッドでは、3 行目の if 文の条件式で self.board[0][0]"〇" であるかどうかを判定しています。self に代入されている Marubatsu クラスのインスタンスには、この時点で 一度も place_markmove メソッドを使って マークを配置していない ので、self.board が表す ゲーム盤 には 一つも マークは 配置されていません。従って 3 行目の if 文の 条件式の計算結果False になり、この if 文の ブロック内の 5 ~ 9 行目のプログラムは 実行されません

次に、12 行目の return winner が実行されますが、ここまでの judge メソッドのブロックの処理では、一度も winner という ローカル変数 に値が 代入されていません。そのため、winner という名前は ローカル名前空間登録されていない ので 名前解決が失敗 し、エラーが発生 します。

フローチャート

言葉だけの説明ではわかりづらいと思った方は、プログラムの処理の流れ (flow)を表す フローチャート(flow chart)と呼ばれる図(chart)を書くと良いでしょう。下図は、judge メソッドのブロックの処理の流れを表すフローチャートです。

フローチャートでは、条件分岐 を上図のように ひし形の図形 で記述し、処理長方形の図形 で記述します。上図では、3 ~ 7 行目の 3 つの if 文の入れ子による条件分岐1 つのひし形の図形まとめて表記 しています。また、9 行目の処理を、図の右にある長方形で表記しています。他にもいくつかの図形が使われますが、それらについては必要になった時点で説明します。

フローチャートは JIS(日本工業規格)によって 厳密な記述方法が定められています。興味がある方は下記のリンク先を参照して下さい。

フローチャートの目的 は、プログラムの流れをわかりやすく理解すること なので、個人的には それほど厳密な記述方法にこだわる必要はないと思います。ただし、独自の図形を作ったり、厳密な記述方法から大きく逸脱するような記述方法はあまりお勧めしません。

これはあくまで 筆者の意見 なので、どんな場合でも厳密な記述方法でフローチャートを記述すべきだという意見を否定するものではありません。

本記事でも厳密な記述方法には あまりこだわらず にフローチャートの図を示すことにします。例えば、正式な規格では、上から下と、左から右への流れを表す線には矢印を記述しませんが、本記事のフローチャートでは、分かりやすさを重視 して 全ての線に矢印を記述する ことにします。

グループ でプログラムを作成する場合に 自己流 のフローチャートを記述してグループの仲間と共有する場合は、混乱の元になる ので 規格に厳密に従ったフローチャートを記述したほうが良い でしょう。

なお、フローチャートは、必ず記述しなければならないものでは ありません。処理の流れが複雑でない場合や、ある程度プログラミングに慣れてきた場合などでは、フローチャートを全く記述せずにプログラムを記述することが実際に良くあります。

フローチャートによるエラーの原因の説明

下記は、ゲーム開始時と、0 列に〇を配置した際に judge メソッドを呼び出した場合の処理の流れを示したフローチャートです。

図の上部の凡例は、ゲーム開始時judge メソッドを呼び出した場合の 処理の流れ赤い矢印 で、0 列に 〇を配置した場合処理の流れ緑の矢印 で表記することを表しています。

上図から、ゲーム開始時judge メソッドを呼び出した場合の赤い矢印の処理の流れでは、ローカル変数 winner に一度も値を代入せずにwinner を関数の返り値として返す処理が行われることがわかります。

エラーの修正方法

エラーの原因は、judge メソッドのブロックを実行してから、12 行目の return winner を実行するまでの間でローカル変数 winner 値が代入されていない ことです。従って、12 行目を実行する より前winner に値が 必ず代入される ように修正すれば、このエラーは発生しなくなります。

12 行目を実行する前に、確実に winner に値を代入する方法は、下記のプログラムの 2 行目のように、judge メソッドの 最初の行 で、winner何らかの値を代入しておく ことです。下記の 2 行目の ??? に何を記述するべきかについて考えてみて下さい。

def judge(self):
    self.winner = ???
    if self.board[0][0] == "":
        以下略

judge メソッドは、3 ~ 9 行目の 3 つの if 文によって、ゲーム盤の 0 行に 〇 が 3 つ配置されていた場合に、winnerMarubatsu.CIRCLE を代入する処理を行いますが、それ以外の場合何の処理も行いません。現時点では 0 行目しか調べていませんが、将来的には、上記のプログラムを、〇 が勝利した場合 に、winnerMarubatsu.CIRCLE を代入する 処理に修正します。その場合でも 〇 の勝利以外 の「× の勝利、引き分け、決着がついていない」場合は 何の処理も行いません

その後で × の勝利を判定する プログラムを実装する際も、同様の処理 を行うプログラムを記述します。従って、〇 の勝利でも × の勝利でもない場合、すなわち、「引き分け、決着がついていない場合」 は 何の処理も行われません

その後で 引き分けを判定する プログラムを実装する際も、同様の処理 を行うプログラムを記述します。従って、何の処理も行われない のは、残った 決着がついていない場合だけ です。

上記から、judge メソッドで 何の処理も行われない のは、決着がついていない場合だけ なので、最初に winner に、決着がついていないことを表す None を代入すれば良い ことがわかります。下記のプログラムはそのように修正したプログラムです。

別の言葉で説明すると、ローカル変数 winner最初は 決着がついていないことを表す None を代入しておき勝者が判定できた場合 に、その値を勝者を表す値で 上書きする ということです。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # (0, 0) が 〇 であることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE:
        # さらに、(1, 0) が 〇 であることを判定する
        if self.board[1][0] == Marubatsu.CIRCLE:
            # さらに、(2, 0) が 〇 であることを判定する
            if self.board[2][0] == Marubatsu.CIRCLE:
                # 〇 が勝利する
                winner = Marubatsu.CIRCLE
  
    # winner を返り値として返す
    return winner

Marubatsu.judge = judge
修正箇所
def judge(self):
+   # 判定を行う前に、決着がついていないことにしておく
+   winner = None
    # (0, 0) が 〇 であることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE:
        # さらに、(1, 0) が 〇 であることを判定する
        if self.board[1][0] == Marubatsu.CIRCLE:
            # さらに、(2, 0) が 〇 であることを判定する
            if self.board[2][0] == Marubatsu.CIRCLE:
                # 〇 が勝利する
                winner = Marubatsu.CIRCLE
    
    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

下記は、先程の確認を行うプログラムと同じものです。実行結果から、今度は エラーが発生せず、ゲームの 開始直後judge メソッドの返り値が、決着がついていないことを表す None である こと、ゲーム盤の 0 行に 〇 を配置した後judge メソッドの返り値が、〇 が勝利したことを表す "o" になる ことが確認できます。

mb = Marubatsu()

# ゲーム開始直後に judge メソッドを呼び出して表示する
print(mb.judge())

# 0 列に 〇 を配置してから judge メソッドを呼び出して表示する
mb.place_mark(0, 0, Marubatsu.CIRCLE)
mb.place_mark(1, 0, Marubatsu.CIRCLE)
mb.place_mark(2, 0, Marubatsu.CIRCLE)
print(mb.judge())

実行結果

None
o

下図は、エラーを修正した場合の judge のフローチャートです。図から、どちらの場合 でも、ローカル変数 winner を関数の返り値として返す処理を 行う前 に、winner に必ず 何らかの値が代入される ことが確認できます。

他にも、下記のプログラムのように、else を記述 し、その中で winner = None を記述するという修正方法があります。この方法では、if 文の条件式が、どのような計算結果になっても、必ず どこかで winner に何らかの値を代入 します。

この方法の場合は、else と winner = None を 3 箇所に記述する必要があるという欠点がありますが、その欠点は次で説明する論理演算子を使うことで解消することができます。この方法については次回以降の記事で紹介します。

def judge(self):
    # (0, 0) が 〇 であることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE:
        # さらに、(1, 0) が 〇 であることを判定する
        if self.board[1][0] == Marubatsu.CIRCLE:
            # さらに、(2, 0) が 〇 であることを判定する
            if self.board[2][0] == Marubatsu.CIRCLE:
                # 〇 が勝利する
                winner = Marubatsu.CIRCLE
            else:
                winner = None
        else:
            winner = None
    else:
        winner = None

    # winner を返り値として返す
    return winner
修正箇所
def judge(self):
    # (0, 0) が 〇 であることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE:
        # さらに、(1, 0) が 〇 であることを判定する
        if self.board[1][0] == Marubatsu.CIRCLE:
            # さらに、(2, 0) が 〇 であることを判定する
            if self.board[2][0] == Marubatsu.CIRCLE:
                # 〇 が勝利する
                winner = Marubatsu.CIRCLE
+           else:
+               winner = None
+       else:
+           winner = None
+   else:
+       winner = None

    # winner を返り値として返す
    return winner

下図は、上記のプログラムの場合のフローチャートです。図から、どちらの場合でも、ローカル変数 winner を関数の返り値として返す処理を行う前に、winner に必ず何らかの値が代入されることが確認できます。

論理演算子

先程のプログラムでは、複数の条件式同時に満たされる ことを判定するために、if 文を入れ子にしたプログラムを記述しましたが、そのようなプログラムは 記述するのが大変 です。このような場合は、and という 論理演算子 を使うと便利です。

論理演算子 とは、論理型 のデータである TrueFalse に対する演算を行う演算子です。そのような演算を ブール(boolean)演算 と呼ぶので、論理演算子の事を ブール演算子 または ブーリアン演算子 と呼ぶ場合があります。同様に、論理型 のことを ブール型 または ブーリアン型 と呼ぶ場合があります。

実際には、論理型以外のデータに対しても、論理演算子を使って演算を行うことが出来ます。そのような場合については必要になった時点で説明します。

Python には、andornot という 3 種類 の論理演算子があります。

and 演算子

and は、日本語では「なおかつ」という意味を表すような演算を行う論理演算子で、前後の値共に True の場合は True に、どちらか片方でも False の場合は False になるという計算を行う演算子です。このことを表にすると以下のようになります。

True False
True True False
False False False

下記は、上記の 4 通りの計算結果を表示するプログラムです。

print(True and True)
print(True and False)
print(False and True)
print(False and False)

実行結果

True
False
False
False

or 演算子

or は、日本語では「または」という意味を表すような演算を行う論理演算子で、前後の値いずれか True の場合は True に、共に False の場合は False になるという計算を行う演算子です。このことを表にすると以下のようになります。

True False
True True True
False True True

下記は、上記の 4 通りの計算結果を表示するプログラムです。

print(True or True)
print(True or False)
print(False or True)
print(False or False)

実行結果

True
True
True
False

not 演算子

not は、日本語で「~でない」という意味の演算を行う論理演算子で、後ろの値True の場合は False に、False の場合は True になるという計算を行う演算子です。このことを表にすると以下のようになります。

True False
False True

下記は not を使った 2 通りの計算結果を表示するプログラムです。

print(not True)
print(not False)

実行結果

False
True

論理演算子の優先順位

論理演算子、四則演算子、比較演算子を 一つの式で同時に記述 した場合は、下記のような 優先順位 で演算が行われます。上に記述された演算子のほうが優先されます。同じ行 に記述された演算子は 記述された順番 で演算が行われます。

  1. * /
  2. + -
  3. == != <= >= < >
  4. not
  5. and
  6. or

例えば 1 + 2 * 3 >= 4 or 5 > 6 and not True という式は、下記の表のような手順で計算が行われます。厳密には、この後で説明する短絡評価という仕組みによって下記とは若干異なる手順で計算が行われますが、ほとんどの場合 は下記の計算手順で計算を行っても 計算結果は変わらない ので、まずは下記の 計算の手順を理解 して下さい。

演算子 計算結果
* 1 + 6 >= 4 or 5 > 6 and not True
+ 7 >= 4 or 5 > 6 and not True
>= > True or False and not True
not True or False and False
and True or False
or True

下記のプログラムは、上記の式の計算結果を表示するプログラムです。

print(1 * 2 + 3 >= 4 or 5 > 6 and not True)

実行結果

True

四則演算と比較演算子の優先順位は、小学校から慣れ親しんでいるので理解するのは困難ではないと思いますが、論理演算子と他の演算子 の優先順位は 慣れないとわかりづらく、優先順位を 間違える ことが バグの原因 となることが非常に良くあります。

そこで、下記のプログラムのように、記述する 必要がない場合 でも () を使って 計算の順番明確にする ことが良くあります。

print((1 * 2 + 3 >= 4) or ((5 > 6) and (not True)))

実行結果

False

Python の演算子の優先順位の詳細については、下記のリンク先を参照して下さい。

and 演算子と or 演算子の短絡評価

and 演算子 で連結された式は、1 つでも 計算結果が False になる と、全体の式の計算結果が False になります。従って、and 演算子で連結された 式を計算する過程 で、計算結果が False になる 式が見つかった場合 は、その時点で 全体の式の計算結果は False になる ので、残りの式 を計算する 必要はありません

or 演算子 で連結された式も 同様 に、1 つでも 計算結果が True になる と、全体の式の計算結果が True になります。計算結果が True になる 式が見つかった場合残りの式 を計算する 必要がない 点も 同様 です。

例えば、先程の 1 + 2 * 3 >= 4 or 5 > 6 and not True という式は、下記の表のような手順で計算が行われると説明しました。しかし、この式は、先頭の 1 + 6 >= 4 の式が or 演算子で連結 されているので、1 + 6 >= 4 の計算結果が True である ことが 分かった時点 で、残りの式を計算しなくても True になる ことが確定します。下記の表の手順で行っている、5 > 6not True の計算を 行う必要はありません

演算子 計算結果
* 1 + 6 >= 4 or 5 > 6 and not True
+ 7 >= 4 or 5 > 6 and not True
>= > True or False and not True
not True or False and False
and True or False
or True

多くのプログラム言語3では、and 演算子連結 された 先頭から順番に計算 し、計算結果が False の式が見つかった 時点で、残りの式を計算しませんor 演算子 も同様に、計算結果が True の式が見つかった 時点で、残りの式を計算しません

このような andor 演算子の 式の演算の省略 のことを 短絡評価 と呼びます。

先程の 1 + 2 * 3 >= 4 or 5 > 6 and not True という式は、実際には 下記の表のような手順で計算が行われます。表からわかるように、1 + 2 * 3 >= 4 の計算結果が True になった時点で 短絡評価 が行われるので、5 > 6not True計算されません

計算結果
先頭の式* 1 + 6 >= 4 or 5 > 6 and not True
先頭の式+ 7 >= 4 or 5 > 6 and not True
先頭の式>= True or 5 > 6 False and not True
短絡評価 True

短絡評価を行っても 論理演算子の優先順位変わらない 点に注意が必要です。例えば、1 > 2 or 3 < 4 and 5 > 6 という式は下記の表のような手順で計算されます。

計算結果
先頭の式 False or 3 > 4 and 5 > 6
真ん中の式 False or True and 5 > 6
右の式 False or True and False
and False or False
or False

真ん中の式を計算した結果、式の前半が False or True となるので計算結果は True となると 勘違いする 人がいるかもしれませんが、and 演算子の方 が、or 演算子より 優先順位が高い ので、先に True and 5 > 6 を計算する必要があります。従って、上記の表の手順で短絡評価が行われずに計算が行われ、計算結果は False になります。

下記は、プログラムで上記の式を計算したもので、実行結果は False になります。

print(1 > 2 or 3 < 4 and 5 > 6)

実行結果

False

短絡評価は、無駄な演算を行わない という処理なので、基本的 には 短絡評価を行わずに計算を行う 場合と比べて、計算結果が変わることはありません。例えば、and や or 演算子で連結された式の中に、関数呼び出し一つも記述されていない 場合は、短絡評価を行った場合と、行わない場合の計算結果は ほとんどの場合で同じになります

ただし、関数呼び出しの返り値を使って計算を行うような式を記述した場合などでは、短絡評価のことを考慮に入れずに プログラムを 記述すると 思わぬ バグが発生する場合 があります。他にも、and や or 演算子で連結された 式の順番を間違えるエラーが発生する 場合があります。それらの具体例については必要になった時点で説明します。

本記事では、短絡評価 のことを 考慮 してプログラムを記述する 必要がある 場合については、そのことを言及する ことにします。言及がない場合 は短絡評価のことを 考慮しなくても良い 場合だと考えて下さい。

下記のような、実引数の値を print で表示 し、実引数の値 をそのまま 返り値として返す p という関数を定義する事で、短絡評価が行われていることを 実際に確認する ことが出来ます。この関数の名前を 1 文字の短い名前にしたのは、この後で 1 行の文の中で何度もこの関数を呼び出して利用するからです。

def p(x):
    print(x)
    return x

例えば、先程の 1 + 2 * 3 >= 4 or 5 > 6 and not True という式で記述されている それぞれのデータ が、実際に この式の計算を行う際に 使われているか を、下記のプログラムのように、それぞれの値を p の実引数で呼び出すようにすることで確認することが出来ます。なお、p で表示した値区別できる ように、この式の計算結果先頭に answer = を表示する ようにしています。

print("answer = ", p(1) + p(2) * p(3) >= p(4) or p(5) > p(6) and not p(True))

実行結果

1
2
3
4
answer =  True

p という関数は、実引数の値を表示 し、そのまま返り値として返す関数なので、式の計算で 実際に使われた値実行結果に表示される ことになります。実行結果から、or と and で連結された 最初の式 に記述された 1234 が表示されているので、最初の式の計算 は、確かに 行われた ことが確認できます。一方、残りの式 に記述された 56True は実行結果に 表示されない ので、5 > 6not True の計算は 行われていない ことが確認できます。

短絡評価のフローチャート

下図は、下記の and と or 演算子を連結した if 文で行われる処理のフローチャートです。短絡評価 によって、式2 と 式3 の計算が省略 される 場合がある ことを確認して下さい。

if 式1 and 式2 and 式3:
    処理
if 式1 or 式2 or 式3:
    処理

下図の左は、下記の and と or 演算子が混在する場合のフローチャートです。図では、優先順位の高い and 演算子 の処理の流れの部分を 赤色の図形 で表記しています。

分かりづらいと思った方は、式2 and 式3 and 式4 の部分を and の部分 のひし形の 図でまとめた 下図の右のフローチャートを見て下さい。下図の右は、先ほどの、or 演算子のみ で連結した場合のフローチャートと 同じ構造 になっています。

if 式1 or 式2 and 式3 and 式4 or 式5:
    処理

and 演算子を使った judge の修正

judge メソッドは、and 演算子を使う ことで、下記のプログラムのように if 文を 1 つだけ記述 するように修正することが出来ます。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 0 行に 〇 が並んでいることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE and self.board[1][0] == Marubatsu.CIRCLE and self.board[2][0] == Marubatsu.CIRCLE:
        # 〇 が勝利する
        winner = Marubatsu.CIRCLE
  
    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

上記のように、条件式が長くなる とプログラムが 読みづらく なります。そのような場合は、下記のプログラムのように、行の最後半角の \(バックスラッシュ。日本語のキーボードでは で入力します) を記述することで、式の途中で改行 を行うことが出来ます。\ を記述せずに改行 すると エラーになる 点に注意して下さい。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 0 行に 〇 が並んでいることを判定する
    if self.board[0][0] == Marubatsu.CIRCLE and \
       self.board[1][0] == Marubatsu.CIRCLE and \
       self.board[2][0] == Marubatsu.CIRCLE:
        # 〇 が勝利する
        winner = Marubatsu.CIRCLE
  
    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

記事が長くなるのと、先ほどと同じ内容 なので記述は省略しますが、先程と同様の方法で、上記のプログラムが正しく動作するかどうかを確認することが出来ます。プログラムの動作の確認は、Marubatsu.ipynb の方では行っているので、そちらを見て下さい。

Python の比較演算子の連鎖による and 演算子の省略

一般的なプログラム言語では、3 つ以上の式 の計算結果が 等しい ことを 判定する ためには、and と同じ意味4 を持つ 論理演算子 を利用する必要があります。例えば、変数 abc の 3 つの値が等しいかどうかを判定するために、a == b == c のような式を記述して判定することは、一般的なプログラム言語 では できません5

一方、Python では、そのような式を記述して、複数の式の値が等しい ことを 簡潔に記述する ことが出来ます。

このような、複数の式比較演算子でつなぐ 事を、比較演算子の 連鎖 と呼びます。比較演算子の連鎖の詳細については、下記のリンク先を参照して下さい。

具体的には、Python では、式1 比較演算子1 式2 比較演算子2 式3 という式は、式1 比較演算子1 式2 and 式2 比較演算子2 式3 とほぼ同じ計算が行われます。

実際には、上記の 2 つの式は 微妙に異なる処理 が行われます。その違いは、式1 比較演算子1 式2 and 式2 比較演算子2 式3 では、式22 回記述 されているので、式2 の計算が 2 回行われる場合があります が、式1 比較演算子1 式2 比較演算子2 式3 の場合は、式2 の計算は 1 回しか行われません

この違いは 多くの場合 は計算結果に 影響を与えません。例えば、式の中に 関数呼び出し記述されていない場合 は、ほとんどの場合で同一の計算 が行われますが、そうでない場合に計算結果が 異なる場合がある ので 注意する必要 があります。現時点のプログラムでは、この違いは計算結果に影響は与えません。また、この違いを説明すると長くなるので今回の記事では説明は省略し、この違いが重要になるような場合が出てきた時点で詳しく説明します。

比較演算子の連鎖 を利用することで、judge メソッドの if 文を下記のプログラムのように修正して 簡潔に記述 することが出来ます。プログラムの動作確認は省略します。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 0 行に 〇 が並んでいることを判定する
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE:
        # 〇 が勝利する
        winner = Marubatsu.CIRCLE
  
    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

残りの 〇 の勝利の判定

残りの 7 種類の 〇 の勝利の判定は、同様の方法 で下記のプログラムのように if 文を並べて記述 することが出来ます。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 0 行の 〇 の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 1 行の 〇 の勝利の判定
    if self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 2 行の 〇 の勝利の判定
    if self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 0 列の 〇 の勝利の判定
    if self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 1 列の 〇 の勝利の判定
    if self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 2 列の 〇 の勝利の判定
    if self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 右下方向の斜めの 〇 の勝利の判定
    if self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
    # 左下方向の斜めの 〇 の勝利の判定
    if self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE

    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

上記の 8 つの if 文は どのブロックでも同じ処理 を行うので、1 つにまとめたほうが良い でしょう。〇 が勝利する条件は、8 つの if 文の条件式の いずれかが True の場合なので、先ほど説明した or 演算子 を使って下記のプログラムのように judge メソッドを修正することが出来ます。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 〇 の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE or \
       self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE or \
       self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE or \
       self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE

    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

本当は、上記のプログラムが正しく動作するかどうかについての 確認を行うべき なのですが、上記のような 複雑な if 文の 条件式 が記述されたプログラムの確認はこれまでのように 簡単に行うことはできません。上記のプログラムの動作の確認については次回の記事で行うことにします。

× の勝利の判定

× の勝利の判定は、下記のプログラムのように、〇 の勝利判定と 同様の方法 で記述することが出来ます。下記のプログラムの確認も次回の記事で行います。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 〇 の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE or \
       self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE or \
       self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE or \
       self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
        
    # × の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CROSS or \
       self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CROSS or \
       self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CROSS or \
       self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CROSS or \
       self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:
        winner = Marubatsu.CROSS        

    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

引き分けの判定

下記の仕様 7 から、全てのマスが埋まったかどうか を調べることで引き分けの判定を行うことが出来ます。

7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

(0, 0) のマスが埋まっているということは、〇 または × のマークが 配置 されているということなので、下記のような if 文を記述することで (0, 0) のマスが埋まっているかどうかを判定することが出来ます。

if self.board[0][0] == Marubatsu.CIRCLE or self.board[0][0] == Marubatsu.CROSS:

残りの 8 マスに対しても同じ条件が満たされている場合に引き分けになるので、下記のプログラムのような条件式を記述すれば 引き分け であることを 判定 することが出来ます。and 演算子 のほうが or 演算子 より 優先順位が高い ので、各マスの判定を行う条件式を () で囲う 必要がある点に注意が必要です。

if (self.board[0][0] == Marubatsu.CIRCLE or self.board[0][0] == Marubatsu.CROSS) and \
   (self.board[1][0] == Marubatsu.CIRCLE or self.board[1][0] == Marubatsu.CROSS) and \
   
   (self.board[2][2] == Marubatsu.CIRCLE or self.board[2][2] == Marubatsu.CROSS):
   self.winner = Marubatsu.DRAW

上記の式は長くて記述するのが大変ですが、別の見方 をすることで、上記の式を 短く 記述することが出来ます。ただし、上記の式が間違っている わけではない ので、上記の式のほうがわかりやすいと思った方は、上記の式を 採用しても問題はありません

全てのマスが埋まっているということは、逆の見方 をすると 空いているマス存在しない ということです。つまり、空いているマスが 1 つでもあれば 引き分けではない ということです。下記は、引き分け でない ことを判定する if 文です。

if self.board[0][0] == Marubatsu.EMPTY or \
   self.board[1][0] == Marubatsu.EMPTY or \
   
   self.board[2][2] == Marubatsu.EMPTY:

引き分けの場合 は、上記の 条件式False になる なので、下記のプログラムのように、not 演算子 を使うことで、引き分けを判定 することが出来ます6not 演算子or 演算子 よりも 優先順位が高い ので、not の後の式の全体を () で囲う必要がある 点に注意が必要です。

if not(self.board[0][0] == Marubatsu.EMPTY or \
       self.board[0][0] == Marubatsu.EMPTY or \
       
       self.board[0][0] == Marubatsu.EMPTY):
    winner = Marubatsu.DRAW

not 演算子がわかりづらいと思った方は、下記のように、False と等しいかどうか を判定するような条件式を記述してもかまいません。

if (self.board[0][0] == Marubatsu.EMPTY or \
    self.board[0][0] == Marubatsu.EMPTY or \
    
    self.board[0][0] == Marubatsu.EMPTY) == False:
    winner = Marubatsu.DRAW

勝敗判定を行うプログラム

上記をまとめると、judge メソッドを以下のように定義する事で勝敗判定を行うことができます。下記のプログラムの確認も次回の記事で行います。

def judge(self):
    # 判定を行う前に、決着がついていないことにしておく
    winner = None
    # 〇 の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE or \
       self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE or \
       self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE or \
       self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE or \
       self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
        winner = Marubatsu.CIRCLE
        
    # × の勝利の判定
    if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CROSS or \
       self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CROSS or \
       self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CROSS or \
       self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CROSS or \
       self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS or \
       self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:
        winner = Marubatsu.CROSS     

    # 引き分けの判定
    if not(self.board[0][0] == Marubatsu.EMPTY or \
           self.board[1][0] == Marubatsu.EMPTY or \
           self.board[2][0] == Marubatsu.EMPTY or \
           self.board[0][1] == Marubatsu.EMPTY or \
           self.board[1][1] == Marubatsu.EMPTY or \
           self.board[1][1] == Marubatsu.EMPTY or \
           self.board[0][2] == Marubatsu.EMPTY or \
           self.board[1][2] == Marubatsu.EMPTY or \
           self.board[2][2] == Marubatsu.EMPTY):
        winner = Marubatsu.DRAW

    # winner を返り値として返す
    return winner

Marubatsu.judge = judge

judge メソッドのバグについて

judge メソッドのように、ある程度複雑 な処理を行うプログラムを実装した場合は、正しく動作するかどうかを 確認する ことが 特に重要 になります。

既に気づいている人がいるかもしれませんが、そのことを 実感できるようにする ために、上記の judge メソッドには わざと いくつのか バグを入れてあります。次回の記事では、それらのバグを、関数のテスト を行うことで 見つける方法 について説明します。余裕があれば、上記のプログラムにどのようなバグがあるかについて考えてみて下さい。バグのヒントを見たいは、下記をクリックして下さい。

バグのヒント

下記の 3 つのバグがあります。なお、3 つ目のバグを見つけるのは初心者にはかなり難しいかもしれません。

  • 単純な入力ミスが原因のバグが 2 つ
  • いずれかの判定を行う方法が間違っていることが原因のバグ

今回の記事で作成したプログラムについて

これで勝敗判定を行う judge メソッドが実装できましたが、実装があまりにも 長くまだるっこしい と思った人が多いのではないかと思います。また、ある程度のプログラムの知識がある方は、もっと 効率的な実装方法 を思いついた人が多いかもしれません。

もちろん、もっと効率的な実装ができる人はそのような方法でいきなり judge メソッドを実装しても構いませんが、上記のような実装が 間違っているわけではありません

また、効率を重視 したプログラムは、どうしても記述したプログラムが わかりづらくなる という 欠点 があります。その点では、上記のような 効率を考えず に、行いたいこと をそのまま 地道に記述する プログラムは、プログラムが長くなるという欠点はありますが、何を行っているかが明確 なため、初心者にとってはバグが発生した場合でもどこにバグがあるかを見つけることが 比較的容易 であるという利点があります。

そもそも、正しく動作する プログラムを記述することができなければ 先に進むことは不可能です。初心者のうちは、最初は効率を考えず にプログラムを記述し、プログラムが正しく動作する事が 確認できてから 効率の良いプログラムになるように 修正する という方法があるということは覚えておいてください。

他の勝敗判定を行うプログラムの実装方法については、次回以降の記事で説明します。

複数のデータ型に対する型アノテーション

今回の記事で実装した judge メソッドは、文字列型 と、None 型 の 2 種類のデータ型を返す関数です。そのような場合は、過去の記事の 関数の引数と返り値のデータ型のヒント(型アノテーション)の表記方法 で説明した方法では、関数の返り値の 型アノテーション を記述することが出来ません。

複数のデータ型 を表す型アノテーションは、下記のプログラムのように、複数のデータ型を表す文字を、半角の |7で区切って表記します。なお、| は、Shift キーを押しながら、 が印字されたキーを押すことで入力することが出来ます。

def judge(self) -> str | None:

型アノテーションで使用する | は Python の バージョン 3.10 で追加された機能 なので、3.9 以前 のバージョンで記述すると エラーが発生します。ただし、3.7 から 3.9 のバージョンでは from __future__ import annotations を記述することで利用できるようになります。

次回以降の記事からは、3.7 から 3.9 のバージョンを使っている人で エラーが発生しないようにするため に、marubatsu.py の 先頭に from __future__ import annotations を記述する ことにします。3.6 以前のバージョンを使っている方は、この機会に最新の python のバージョンの仮想環境を作成することをお勧めします。

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

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/11/25 データ型のヒントを、型アノテーションに修正しました
  1. 今回の記事では紹介しませんが、list や dict を使って複数のデータを返すという方法も良く使われます

  2. bound(束縛)とは、対応づけ を表すプログラム用語です。従って、unbound local とは、ローカル変数がオブジェクトに対応付けられていない(unbound)こと、すなわち名前空間に登録されていないことを意味します

  3. Microsoft Office で使われる VBA というプログラム言語など、一部のプログラム言語では、and と or の論理演算子で短絡評価を行わないものもあるようです

  4. プログラム言語によって、論理演算子を表す 表記が異なる 場合があります。例えば、C 言語や JavaScript では、and 演算子を && のように表記します

  5. 詳細は長くなるので省略しますが、そのような式を他のプログラム言語で記述しても エラーにはなりません が、Python の場合とは 全く異なる計算 が行われます

  6. 最初の and 演算子を使った条件式を、or 演算子と not 演算子を組み合わせて置き換える際に、ド・モルガンの法則 という法則を使っています。参考までにド・モルガンの法則の wikipedia の リンク を貼っておきます

  7. | は、パイプ(pipe)、バーチカルバー(垂直(vertical)の棒(bar))など、いくつかの呼び方があります

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