6.4 継承とオーバーライド

(1/1)
カードをシャフルして配る(GUI版)
前々回作成した Python のカードゲーム基本クラス "pahooCardGame" はポーカーを想定していると書いた。今回は、このクラスを継承し、ポーカーの役判定を行うメソッドを追加する。
前回に続き、pywebview を利用することで、グラフィカルな画面操作ができるようにする。
最後に、関数やメソッドを別の関数やメソッドで上書き(オーバーライド)する目的と、その方法について学ぶ。

目次

サンプル・プログラム

継承

クラス図
クラスを備えたオブジェクト指向型プログラミング言語には、たいてい、クラスを継承する機能が備わっている。継承とは、すでにあるクラスを拡張し、あらたなクラスを作ることである。継承元のクラスを基底クラス、基底クラスを元にあらたに作成したクラスを派生クラスと呼ぶ。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。

そこで今回は、前々回作成したのカードゲーム基本クラス "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,...):" のように基底クラス名をカンマで区切る。

ポーカーの役を判定する

クラスを備えたオブジェクト指向型プログラミング言語には、たいてい、クラスを継承する機能が備わっている。継承とは、すでにあるクラスを拡張し、あらたなクラスを作ることである。継承元のクラスを基底クラス、基底クラスを元にあらたに作成したクラスを派生クラスと呼ぶ。
継承を活用することで、それまで使っていたクラスを変更することなく、あらたなメソッドや変数を備えた派生クラスを作成することができ、プログラムの生産性・保守性が高まる。
ポーカーには、下表の通り9つの役がある。
ポーカーの役一覧
強さ役の名称スートナンバー
連続性同じ
1ロイヤルフラッシュ同じ連続
1,10,11,12,13
2ストレートフラッシュ同じ連続
3フォーカード4枚が同じ
4フルハウス3枚が同じ
+2枚が同じ
5フラッシュ同じ
6ストレート連続
7スリーカード3枚が同じ
8ツーペア2枚が同じ
+別の2枚が同じ
9ワンペア2枚が同じ
こうしてみると、9つの役は3種類に分類することができる。
  1. フラッシュ系:スートが同じ(ロイヤルフラッシュ、ストレートフラッシュ、フラッシュ)
  2. ストレート系:ナンバーが連続している(ロイヤルフラッシュ、ストレートフラッシュ、ストレート)
  3. ペア系:同じナンバーがある(フォーカード、フルハウス、スリーカード、ツーペア、ワンペア)
これまで学んだコンテナデータ型を活用することで、9つの役を判定する9つのメソッドを比較的短いプログラムで作ることができる。
まず、フラッシュ系を判定するために、手札のスートを集合(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つしかないことになる。
フラッシュ
	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 を返す。
	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集合に格納する。
ストレート
	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 に変化させ、合致すればストレートだ。
ストレートフラッシュ
	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)として返す。
ワンペア
	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 (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:
    ....
ここで、もしローカルルールの役を追加しようとすると、その分だけ elif が増え、制御が複雑になる。
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 再帰呼び出し」では繰り返し制御を使わない手法を提示したが、これとあわせて、制御文を使わない関数型プログラミングができるようになる。

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 にあるメソッド shuffleStocksstocks2hands は派生クラス pahooCardGamePoker に継承され、そのまま使うことができる。
HTMLとJavaScriptで書いた "cardGame3.html" も、前回の "cardGame2.html" からタイトルを変更した程度である。

このように継承を活用することで、最小限のプログラム変更で新しい機能を持ったプログラムを作ることができる
さて、ここまでで紹介したクラスやデータ構造は、複数のプレイヤーに対応している。プレイヤーIDの0番を親としてコンピュータに担当させ、クラウドサービスのポーカーゲームに拡張することができるだろう。また、インスタンスが1つにつき1つのカードセットに対応しているから、インスタンスを複数生成してリストなどで管理してやれば、クラウド上にカードゲーム場を開設することもできるだろう。応用としてチャレンジしてほしい。

オーバーライド

カードゲームの話は、いったん終わる。
さて、標準ライブラリ 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 では、関数だけでなくメソッドもオーバーライドできる。派生クラスで基底クラスと同じメソッドをオーバーライドすることで、派生クラスの変更を吸収することができる。

練習問題

次回予告

ユーザー定義クラスを再利用したり保守をしやすくする目的で、そのクラスを利用するプログラマにとって必要のないデータを操作しないように隠蔽することをカプセル化と呼ぶ。そのクラス内でしか使わないようなインスタンス変数を隠したり、直接インスタンス変数にアクセスしないようにすることが行われる。
次回は、カプセル化の手法に加え、隠蔽したインスタンス変数を操作するのに便利なプロパティや、ポリモーフィズムについて学ぶ。

コラム:基底クラスと派生クラス

基底クラスと派生クラス
本文では、多くのカードゲームは52枚(ジョーカーを除く)で行われ、山札、場札、手札があり、山札から手札を配り、手札から場札に捨てるという基本操作を基底クラスとし、ゲーム固有の機能を加えたものを派生クラスとした。

現実に存在するオブジェクトに目を向けると、たとえば「自動車」という基底クラスは、「走る」「四輪」という共通項目がある。これから派生クラスとして「トラック」を作ったとすると、「荷物を載せる」というメソッドが追加になるだろう。また、派生クラスとして「バス」を作ったとすると、「乗客を乗せる」というメソッドが追加になるだろう。

クラスを設計するとき、何が共通で、何が固有なのかを仕分ける抽象化能力が求められる。そういう意味では、プログラム設計をする技術者は、コーディング能力よりも、事物の分析能力、場合によっては業務やルール(ゲームを含む)について通暁している必要がある。
(この項おわり)
header