サンプル・プログラム
継承
クラスを備えたオブジェクト指向型プログラミング言語には、たいてい、クラスを継承する機能が備わっている。継承とは、すでにあるクラスを拡張し、あらたなクラスを作ることである。継承元のクラスを基底クラス、基底クラスを元にあらたに作成したクラスを派生クラスと呼ぶ。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。
そこで今回は、前々回作成したのカードゲーム基本クラス "pahooCardGame" を基底クラスとし、ポーカーの役判定メソッドを備えた派生クラス "pahooCardGamePoker" を作成することで継承について学ぶ。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。
そこで今回は、前々回作成したのカードゲーム基本クラス "pahooCardGame" を基底クラスとし、ポーカーの役判定メソッドを備えた派生クラス "pahooCardGamePoker" を作成することで継承について学ぶ。
クラスを設計するときは、上図のようなクラス図で表す。基底クラスが上、派生クラスは下に書き、図のような矢印で接続する。各クラスの上方にはクラス変数やメソッド変数を、下方にはメソッドを記入する。
from collections import Counter
from pahooCardGame import pahooCardGame
class pahooCardGamePoker(pahooCardGame):
"""カードゲーム:ポーカー・クラス
Copyright:
(c)studio pahoo, パパぱふぅ
Environment:
Python 3.7 or after
Note:
Python入門 - 6.4 継承とオーバーライド
https://www.pahoo.org/e-soul/webtech/python00/py00-06-04.html
pahooCardGameクラスを継承.
ポーカーの役を判定する.
"""
派生クラス "pahooCardGamePoker" を格納したファイル "pahooCardGamePoker.py" をご覧いただきたい。
まず、"import文" で基底クラスを格納したファイル "pahooCardGame.py" を呼び出す。
次に派生クラスの定義だが、"class 派生クラス名(基底クラス名):" のようにして行う。
ここでは基底クラスと派生クラスが1対1対応だが、もし複数の基底クラスから1つの派生クラスを定義するのであれば、"class 派生クラス名(基底クラス名1, 基底クラス名2,...):" のように基底クラス名をカンマで区切る。
まず、"import文" で基底クラスを格納したファイル "pahooCardGame.py" を呼び出す。
次に派生クラスの定義だが、"class 派生クラス名(基底クラス名):" のようにして行う。
ここでは基底クラスと派生クラスが1対1対応だが、もし複数の基底クラスから1つの派生クラスを定義するのであれば、"class 派生クラス名(基底クラス名1, 基底クラス名2,...):" のように基底クラス名をカンマで区切る。
ポーカーの役を判定する
クラスを備えたオブジェクト指向型プログラミング言語には、たいてい、クラスを継承する機能が備わっている。継承とは、すでにあるクラスを拡張し、あらたなクラスを作ることである。継承元のクラスを基底クラス、基底クラスを元にあらたに作成したクラスを派生クラスと呼ぶ。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。
ポーカーには、下表の通り9つの役がある。
強さ | 役の名称 | スート | ナンバー | |
---|---|---|---|---|
連続性 | 同じ | |||
1 | ロイヤルフラッシュ | 同じ | 連続 1,10,11,12,13 | - |
2 | ストレートフラッシュ | 同じ | 連続 | - |
3 | フォーカード | - | - | 4枚が同じ |
4 | フルハウス | - | - | 3枚が同じ +2枚が同じ |
5 | フラッシュ | 同じ | - | - |
6 | ストレート | - | 連続 | - |
7 | スリーカード | - | - | 3枚が同じ |
8 | ツーペア | - | - | 2枚が同じ +別の2枚が同じ |
9 | ワンペア | - | - | 2枚が同じ |
こうしてみると、9つの役は3種類に分類することができる。
- フラッシュ系:スートが同じ(ロイヤルフラッシュ、ストレートフラッシュ、フラッシュ)
- ストレート系:ナンバーが連続している(ロイヤルフラッシュ、ストレートフラッシュ、ストレート)
- ペア系:同じナンバーがある(フォーカード、フルハウス、スリーカード、ツーペア、ワンペア)
これまで学んだコンテナデータ型を活用することで、9つの役を判定する9つのメソッドを比較的短いプログラムで作ることができる。
まず、フラッシュ系を判定するために、手札のスートを集合(set)として返すメソッドを作る。
まず、フラッシュ系を判定するために、手札のスートを集合(set)として返すメソッドを作る。
def suitSetInHands(self, id):
"""手札のスートを集合として返す.
Args:
id(int): プレイヤーID
Returns:
set: 手札にあるスート
"""
return { suit for suit, number in self.hands[id] }
メソッド "suitSetInHands" は、手札のスートを集合(set)として返す。
プレイヤー id の手札はリスト self.hands[id] だ。for文を使って、このリストの要素((suit, number) のタプル)を1つ1つ取りだし、そのうちの suit を集合に格納する。もし手札がフラッシュ系であれば、この集合には要素が1つしかないことになる。
プレイヤー id の手札はリスト self.hands[id] だ。for文を使って、このリストの要素((suit, number) のタプル)を1つ1つ取りだし、そのうちの suit を集合に格納する。もし手札がフラッシュ系であれば、この集合には要素が1つしかないことになる。
def suitSetInHands(self, id):
"""手札のスートを集合として返す.
Args:
id(int): プレイヤーID
Returns:
set: 手札にあるスート
"""
return { suit for suit, number in self.hands[id] }
メソッド "isFlush" は、手札がフラッシュかどうかを判定する。
前述のメソッド "suitSetInHands" の戻り値の要素の数を len関数 を使って調べ、もし1なら――スートが1種類しかなければ――True を返す。
前述のメソッド "suitSetInHands" の戻り値の要素の数を len関数 を使って調べ、もし1なら――スートが1種類しかなければ――True を返す。
def numberSetInHands(self, id):
"""手札のナンバーを集合として返す.
Args:
id(int): プレイヤーID
Returns:
set: 手札にあるナンバー
"""
return { number for suit, number in self.hands[id] }
次に、ストレート系を判定するために、手札のナンバーを集合(set)として返すメソッド "numberSetInHands" を作る。
前述のメソッド "suitSetInHands" と同様に、for文を使って、手札リスト self.hands[id] の要素を1つ1つ取りだし、そのうちの number を集合に格納する。
前述のメソッド "suitSetInHands" と同様に、for文を使って、手札リスト self.hands[id] の要素を1つ1つ取りだし、そのうちの number を集合に格納する。
def isStraight(self, id):
"""手札がストレートかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
for i in range(1, 10):
numbers = set(range(i, i + 5))
if self.numberSetInHands(id) == numbers:
return True
return False
メソッド "isStraight" は、手札がストレートかどうかを判定する。
range関数と set関数を使い、1からはじまる連続した整数5つ文の集合(set)を生成する。これが手札と合致すればストレートだ。for文 を使い、開始値を1, 2, 3,...10 に変化させ、合致すればストレートだ。
range関数と set関数を使い、1からはじまる連続した整数5つ文の集合(set)を生成する。これが手札と合致すればストレートだ。for文 を使い、開始値を1, 2, 3,...10 に変化させ、合致すればストレートだ。
def isStraightFlush(self, id):
"""手札がストレートフラッシュかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
return self.isStraight(id) and self.isFlush(id)
手札がストレートかつフラッシュであれば、役はストレートフラッシュになる。これを判定するのがメソッド "isStraightFlush" だ。
def isRoyalFlash(self, id):
"""手札がロイヤルフラッシュかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
numbers = { 1, 10, 11, 12, 13 }
return (self.numberSetInHands(id) == numbers) and self.isFlush(id)
手札のナンバーが集合 { 1, 10, 11, 12, 13 } かつフラッシュであれば、役はロイヤルフラッシュになる。これを判定するのがメソッド "isRoyalFlash" だ。集合は順序(シーケンス)が異なっていても同一とみなす性質を利用した。
def countNumbers(self, id):
"""手札のナンバー出現回数を返す.
Args:
id(int): プレイヤーID
Returns:
Counter: ナンバー毎の出現回数
"""
numbers = [number for suit, number in self.hands[id]]
return Counter(numbers)
ペア系かどうかを判定するには、メソッド "countNumbers" を使って手札のナンバーの出現回数を返す。
前述のメソッド "suitSetInHands" と同様に、for文を使って、手札リスト self.hands[id] の要素を1つ1つ取りだし、そのうちの number をリストに格納する。
Python の標準モジュール collections というコンテナデータ型に、Counterサブクラスがある。これは、ハッシュ可能なオブジェクトをカウントする辞書(dict)のサブクラスだ。つまり、識別可能なオブジェクトを辞書のキーとし、その出現回数を要素として保存する。
ここでは、ナンバーをキーとして、その出現回数を要素として保存することで、手札のナンバーの出現回数を辞書(dict)として返す。
前述のメソッド "suitSetInHands" と同様に、for文を使って、手札リスト self.hands[id] の要素を1つ1つ取りだし、そのうちの number をリストに格納する。
Python の標準モジュール collections というコンテナデータ型に、Counterサブクラスがある。これは、ハッシュ可能なオブジェクトをカウントする辞書(dict)のサブクラスだ。つまり、識別可能なオブジェクトを辞書のキーとし、その出現回数を要素として保存する。
ここでは、ナンバーをキーとして、その出現回数を要素として保存することで、手札のナンバーの出現回数を辞書(dict)として返す。
def isOnePear(self, id):
"""手札がワンペアかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
flag = 0
for number, count in self.countNumbers(0).items():
if count == 2:
return True
return False
メソッド "isOnePear" は、前述のメソッド "countNumbers" で得られた手札のナンバー出現回数辞書の要素を1つ1つ調べ、2になるものがあったらワンペアと判定する。
def isTwoPear(self, id):
"""手札がツーペアかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
flag = 0
for number, count in self.countNumbers(0).items():
if count == 2:
flag += 1
if flag == 2:
return True
return False
メソッド "isTwoPear" は、同様に、手札のナンバー出現回数辞書の要素が2になるものが2つあればツーペアと判定する。
def isThreeCards(self, id):
"""手札がスリーカードかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
for number, count in self.countNumbers(0).items():
if count == 3:
return True
return False
メソッド "isThreeCards" は、同様に、手札のナンバー出現回数辞書の要素が3になるものがあればスリーカードと判定する。
def isFourCards(self, id):
"""手札がフォーカードかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
for number, count in self.countNumbers(0).items():
if count == 4:
return True
return False
メソッド "isFourCards" は、同様に、手札のナンバー出現回数辞書の要素が4になるものがあればフォーカードと判定する。
def isFullHouse(self, id):
"""手札がフルハウスかどうか判定する.
Args:
id(int): プレイヤーID
Returns:
bool: 判定結果
"""
flag = 0
for number, count in self.countNumbers(0).items():
if count == 2:
flag += 1
elif count == 3:
flag += 1
if flag == 2:
return True
return False
メソッド "isFullHouse" は、同様に、手札のナンバー出現回数辞書の要素が2になるものと3になるものがあればフルハウスと判定する。
if文を使わずに条件分岐する
これら9つのメソッドを、役の強い順に if文を使って条件分岐していけば、目的のクラスが完成する。
if (self.isRoyalFlash(id)):ここで、もしローカルルールの役を追加しようとすると、その分だけ elif が増え、制御が複雑になる。
....
elif (self.isStraightFlush(id)):
....
elif (self.isFourCards(id)):
....
elif (self.isFullHouse(id)):
....
elif (self.isFlush(id)):
....
elif (self.isStraight(id)):
....
elif (self.isThreeCards(id)):
....
elif (self.isTwoPear(id)):
....
elif (self.isOnePear(id)):
....
else:
....
Python では、メソッドそのものをオブジェクトとみなし、変数に値として代入することができる。この性質を利用し、{ "medhod": メソッド名, "title": 役の名前 } という辞書をつくり、これを9つの要素とするタプルを用意する。
def getPokerHands(self, id):
"""手札の役を返す.
Args:
id(int): プレイヤーID
Returns:
set: 手札にあるスート
"""
# 役判定タプル辞書(役が強い順)
# { method:判定メソッド, title:役の名前 }
methods = (
{ "method": self.isRoyalFlash, "title": "ロイヤルフレッシュ" },
{ "method": self.isStraightFlush, "title": "ストレートフラッシュ" },
{ "method": self.isFourCards, "title": "フォーカード" },
{ "method": self.isFullHouse, "title": "フルハウス" },
{ "method": self.isFlush, "title": "フラッシュ" },
{ "method": self.isStraight, "title": "ストレート" },
{ "method": self.isThreeCards, "title": "スリーカード" },
{ "method": self.isTwoPear, "title": "ツーペア" },
{ "method": self.isOnePear, "title": "ワンペア" })
# 役が強い順に手札の役判定を行う.
for m in methods:
if m["method"](id): return m["title"] # マッチすれば役名を返す
return ""
メソッド "getPokerHands" は、上述のタプル辞書 methods を用意し、for文 を使って強い順に役判定を行う。タプル辞書 methods は役の強い順に並べること。
こうすることで、ローカルルールで役を増減させたとしても、タプル辞書 methods の変更だけで判定が可能になる。
Python では、メソッドと同様、関数そのものをオブジェクトとみなし、変数に値として代入することができる。
メソッドや関数をコンテナに格納することで、長大な if文や match文を書かずにプログラムを作ることができる。「5.3 再帰呼び出し」では繰り返し制御を使わない手法を提示したが、これとあわせて、制御文を使わない関数型プログラミングができるようになる。
こうすることで、ローカルルールで役を増減させたとしても、タプル辞書 methods の変更だけで判定が可能になる。
Python では、メソッドと同様、関数そのものをオブジェクトとみなし、変数に値として代入することができる。
メソッドや関数をコンテナに格納することで、長大な if文や match文を書かずにプログラムを作ることができる。「5.3 再帰呼び出し」では繰り返し制御を使わない手法を提示したが、これとあわせて、制御文を使わない関数型プログラミングができるようになる。
Pythonメイン・プログラム
def execCommand(command, cg, id):
"""コマンドを解釈し実行する.
Args:
command(str): コマンド
cg(cardGame): cardGameオブジェクト
Returns:
bool: True 実行成功 / False 処理終了, str: メッセージ
Note:
終了コマンドでプログラムを終了する.
"""
# 終了コマンド
if (command == "q"):
# webviewウィンドウを開いていれば閉じる
if "window" in globals():
window.destroy()
# プログラム終了
sys.exit()
# やり直しコマンド
elif (command == "c"):
cg.__init__()
cg.shuffleStocks()
cg.stocks2hands(5, id)
return True, "やり直し"
# 交換する手札番号リストを生成
numList = [item.strip() for item in command.split(",")]
maxNum = cg.countHands(id)
nums = []
for ss in numList:
try:
nums.append(pv.validateNumber(ss, "int", 1, maxNum, label="手札"))
except ValueError as errmsg:
return True, errmsg
# 手札番号リストに重複がないかどうかバリデーション
if isDuplivacates(nums): return True, "交換する手札番号が重複しています"
# 手札番号をソートする
nums.sort()
# 手札を1枚ずつ交換する
cnt = 1
for i in nums:
cg.hands2layouts(i - cnt, id)
cnt += 1
if not cg.stocks2hands(1, id): return True, "山札がなくなりました"
print("command = " + command)
print(cg.hands[0])
# ポーカーの役判定
message = cg.getPokerHands(id)
if (message != ""): return True, message
else: return True, f"{nums}を交換しました"
class Api:
def initGame(self):
"""初期化する.
Args:
None
Returns:
None
"""
# 初期化
try:
self.cg = pahooCardGamePoker(1)
self.cg.shuffleStocks()
except ValueError as errmsg:
pv.dispErrorMessageAndExit(errmsg)
self.playerId = 0 # プレイヤーID
self.cg.stocks2hands(5, self.playerId)
print(self.cg.hands)
response = { "hands": self.cg.hands, "message": "開始" }
return json.dumps(response)
def execCommand(self, command):
"""コマンドを解釈し実行する.
Args:
command(str): コマンド
Returns:
None
"""
(res, message) = execCommand(command, self.cg, self.playerId)
if message != "": print(message)
response = { "hands": self.cg.hands, "message": message }
return json.dumps(response)
def openBrowser(self, url):
Python メイン・プログラム "cardGame3.py" を実行してみてほしい。プログラムは、前回の "cardGame2.py" とほとんど同じである。生成するインスタンスが pahooCardGamePoker であることと、コマンド実行時にポーカーの役判定メソッド getPokerHands を追加した程度である。
基底クラス pahooCardGame にあるメソッド shuffleStocks や stocks2hands は派生クラス pahooCardGamePoker に継承され、そのまま使うことができる。
HTMLとJavaScriptで書いた "cardGame3.html" も、前回の "cardGame2.html" からタイトルを変更した程度である。
このように継承を活用することで、最小限のプログラム変更で新しい機能を持ったプログラムを作ることができる。
さて、ここまでで紹介したクラスやデータ構造は、複数のプレイヤーに対応している。プレイヤーIDの0番を親としてコンピュータに担当させ、クラウドサービスのポーカーゲームに拡張することができるだろう。また、インスタンスが1つにつき1つのカードセットに対応しているから、インスタンスを複数生成してリストなどで管理してやれば、クラウド上にカードゲーム場を開設することもできるだろう。応用としてチャレンジしてほしい。
基底クラス pahooCardGame にあるメソッド shuffleStocks や stocks2hands は派生クラス pahooCardGamePoker に継承され、そのまま使うことができる。
HTMLとJavaScriptで書いた "cardGame3.html" も、前回の "cardGame2.html" からタイトルを変更した程度である。
このように継承を活用することで、最小限のプログラム変更で新しい機能を持ったプログラムを作ることができる。
さて、ここまでで紹介したクラスやデータ構造は、複数のプレイヤーに対応している。プレイヤーIDの0番を親としてコンピュータに担当させ、クラウドサービスのポーカーゲームに拡張することができるだろう。また、インスタンスが1つにつき1つのカードセットに対応しているから、インスタンスを複数生成してリストなどで管理してやれば、クラウド上にカードゲーム場を開設することもできるだろう。応用としてチャレンジしてほしい。
オーバーライド
カードゲームの話は、いったん終わる。
さて、標準ライブラリ math の三角関数(sin, cos, tan)の引数の単位はラジアンである。しかし実務では、度を単位とする引数を渡すことが多いだろう。
正弦 sin だけだが、度を引数として渡すことができるプログラムが "mathDegree.py" である。
さて、標準ライブラリ math の三角関数(sin, cos, tan)の引数の単位はラジアンである。しかし実務では、度を単位とする引数を渡すことが多いだろう。
正弦 sin だけだが、度を引数として渡すことができるプログラムが "mathDegree.py" である。
import math
# 元の関数を保存する.
sinRadian = math.sin
def sin(deg):
"""正弦の値を返す.
Args:
deg(float): 角度(度)
Returns:
float: 正弦の値
"""
return sinRadian(math.radians(deg))
# 新しい関数でオーバーライドする.
math.sin = sin
deg = 60 # 60度
print(math.sin(deg))
まず、関数を値として変数に代入できる Python の性質を利用し、元の正弦関数 math.sin を変数 sinRadian に退避する。
次に、度を引数とする新しい正弦関数 sin を定義する。この中では退避した関数 sinRadian を呼び出す。
最後に、math.sin に新しい関数 sin を代入する。これをオーバーライドと呼び、これ以降、math.sin を呼び出すと、新しい関数 sin が実行される。もしオリジナル関数を呼び出したければ、sinRadian を実行する。
Python では、関数だけでなくメソッドもオーバーライドできる。派生クラスで基底クラスと同じメソッドをオーバーライドすることで、派生クラスの変更を吸収することができる。
次に、度を引数とする新しい正弦関数 sin を定義する。この中では退避した関数 sinRadian を呼び出す。
最後に、math.sin に新しい関数 sin を代入する。これをオーバーライドと呼び、これ以降、math.sin を呼び出すと、新しい関数 sin が実行される。もしオリジナル関数を呼び出したければ、sinRadian を実行する。
Python では、関数だけでなくメソッドもオーバーライドできる。派生クラスで基底クラスと同じメソッドをオーバーライドすることで、派生クラスの変更を吸収することができる。
練習問題
次回予告
ユーザー定義クラスを再利用したり保守をしやすくする目的で、そのクラスを利用するプログラマにとって必要のないデータを操作しないように隠蔽することをカプセル化と呼ぶ。そのクラス内でしか使わないようなインスタンス変数を隠したり、直接インスタンス変数にアクセスしないようにすることが行われる。
次回は、カプセル化の手法に加え、隠蔽したインスタンス変数を操作するのに便利なプロパティや、ポリモーフィズムについて学ぶ。
次回は、カプセル化の手法に加え、隠蔽したインスタンス変数を操作するのに便利なプロパティや、ポリモーフィズムについて学ぶ。
コラム:基底クラスと派生クラス
本文では、多くのカードゲームは52枚(ジョーカーを除く)で行われ、山札、場札、手札があり、山札から手札を配り、手札から場札に捨てるという基本操作を基底クラスとし、ゲーム固有の機能を加えたものを派生クラスとした。
現実に存在するオブジェクトに目を向けると、たとえば「自動車」という基底クラスは、「走る」「四輪」という共通項目がある。これから派生クラスとして「トラック」を作ったとすると、「荷物を載せる」というメソッドが追加になるだろう。また、派生クラスとして「バス」を作ったとすると、「乗客を乗せる」というメソッドが追加になるだろう。
クラスを設計するとき、何が共通で、何が固有なのかを仕分ける抽象化能力が求められる。そういう意味では、プログラム設計をする技術者は、コーディング能力よりも、事物の分析能力、場合によっては業務やルール(ゲームを含む)について通暁している必要がある。
現実に存在するオブジェクトに目を向けると、たとえば「自動車」という基底クラスは、「走る」「四輪」という共通項目がある。これから派生クラスとして「トラック」を作ったとすると、「荷物を載せる」というメソッドが追加になるだろう。また、派生クラスとして「バス」を作ったとすると、「乗客を乗せる」というメソッドが追加になるだろう。
クラスを設計するとき、何が共通で、何が固有なのかを仕分ける抽象化能力が求められる。そういう意味では、プログラム設計をする技術者は、コーディング能力よりも、事物の分析能力、場合によっては業務やルール(ゲームを含む)について通暁している必要がある。
(この項おわり)
前回に続き、pywebview を利用することで、グラフィカルな画面操作ができるようにする。
最後に、関数やメソッドを別の関数やメソッドで上書き(オーバーライド)する目的と、その方法について学ぶ。