6.5 カプセル化とポリモーフィズム

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

目次

サンプル・プログラム

カプセル化

class character():
	"""RPGのキャラクター(基底クラス)

	Environment:
		Python 3.7 or after
	
	Attributes:
		name(str):			プレイヤー名
		__parameters(dict):	パラメータ辞書

	"""
	def __init__(self, name):
		self.name = name
		self.__parameters = {}

	def getName(self):
		"""名前を返す
		
		Returns:
			str: 名前
		"""
		return self.name

	def setParameter(self, key, val):
		"""パラメータを1つ設定する

		Args:
			key(str): 辞書キー
			val(int): 設定値
		
		Returns:
			None
		"""
		self.__parameters[key] = val

	def getParameters(self):
		"""パラメータをまとめて返す
		
		Returns:
			dict: パラメータ
		"""
		return self.__parameters
Python プログラム "RPG1.py" をご覧いただきたい。このプログラムはRPGゲームを想定したもので、ユーザー定義クラス "character" はゲームに登場するキャラクターを表す基底クラスである。
インスタンス変数として、プレイヤー名を格納する name と、そのプレイヤーのヒットポイント(HP)やマジックポイント(MP)などのパラメータを格納する辞書 __parameter の2つをもつ。
3.1 変数と命名規則」で「変数名の前後にアンダースコア _ を付けない方が無難」と書いたが、インスタンス変数 __parameter は、後述する通りカプセル化するためにあえてアンダースコア _ を付けている。

初期化メソッドでは、渡されたプレイヤー名 name をインスタンス変数に格納し、次にインスタンス変数 __parameter を初期化(空の辞書を生成)する。
class hero(character):
	"""勇者(派生クラス)
	"""
	def __init__(self, name):
		super().__init__(name)
		parameters = {"HP": 10, "MP": 5, "INT": 5, "AGI": 5, "LV": 0}
		for key, val in parameters.items():
			self.setParameter(key, val)

	def getOccupation(self):
		return "勇者"
ユーザー定義クラス "hero" は "character" を継承した派生クラスで、勇者という職業を表す。
初期化メソッドは、super().__init__(name) を実行する。super() を使うと継承元(基底クラス)のメソッドを直接呼び出すことができる。ここでは、基底クラス character初期化メソッドを呼び出し、プレイヤー名 name をインスタンス変数に格納し、パラメータ辞書を空にする。
つづいて、変数 paramaters に代入した初期パラメータを、基底クラスの setParameterメソッドを使ってインスタンス変数に格納していく。
この他、職業名を返すメソッド getOccupation を用意した。

プログラム "RPG1.py" には、勇者クラス "hero" のほかに、戦士、魔術師、僧侶、盗賊を用意した。いずれも "character" を継承した派生クラスで、職業に応じて初期パラメータを変えてある。
# メイン・プログラム =======================================================
# プレイヤーをリストに代入
players = [hero("ヒンメル"), wizard("フリーレン"), priest("ハイター"), warrior("アイゼン")]

print(f"players[0].getName() = {players[0].getName()}")			# 成功
print(f"players[0].name = {players[0].name}")				# 成功
print(f"players[0].getParameters() = {players[0].getParameters()}")	# 成功
print(f"_players[0]__parameters = {players[0]._character__parameters}")		# エラー
メイン・プログラムを見ていこう。
まず、派生クラス hero, wizard, priest, warrior をインスタンス化し、どこかで見たような4人のプレイヤーをリスト players に格納する。

次に、基底クラスのメソッド getName を使って players[0] の名前を表示する。players[0] は、インスタンス化したときの名前=ヒンメルが表示されるはずだ。
続いて、インスタンス変数 name を直接表示してみる。これも同じ名前が表示されるはずだ。

最後に、ヒンメルのパラメータを表示してみよう。
まず、基底クラスのメソッド getParameters を使って表示する。パラメータ辞書がそのまま表示されるはずだ。
今度は、インスタンス変数 __parameter を直接表示してみる。ところが、これはエラーになってしまう。

Python では、二重アンダースコア __ ではじまるインスタンス変数は、そのクラスやインスタンスの外側からアクセスできないようになっている。
キャラクターのパラメータは、ゲーム進行上の重要な要素なので、直接操作されては困るものだ。ここでは、パラメータ値を1つずつ設定する setParameter と、パラメータ値をまとめて取得する getParameters というメソッド経由でないとパラメータを操作できないようにした。
実際のゲームプログラムでは、setParameter の中でバリデーションチェックしたり、シナリオと矛盾しないパラメータ調整をすることになるだろう。
なお、Python の隠蔽化は、JavaやPHPにおけるprivate修飾子のように完璧に隠蔽できるものではなく、内部的に変数名をリネームしているだけである。たとえばインスタンス変数 __parameters について言えば、_character__parameters にリネームされている。つまり、players[0]._character__parameters と書けば直接アクセスできてしまう。
このようにルールが緩いところも Python の特徴ではあるが、実際のプログラミングでは、隠蔽したインスタンス変数への直接アクセスはやらないというルールを設けておいた方がいいだろう。

プロパティ

インスタンス変数を隠蔽する目的は理解できたと思うが、いちいちメソッドを使って呼び出すのは面倒だ。Python では、隠蔽したインスタンス変数に、あたかも変数にアクセスするようにしてメソッドを呼び出す方法が用意されている。これがプロパティである。
class character():
	"""RPGのキャラクター(基底クラス)

	Environment:
		Python 3.7 or after
	
	Attributes:
		name(str):			プレイヤー名
		__parameters(dict):	パラメータ辞書

	"""
	def __init__(self, name):
		self.name = name
		self.__parameters = {}

	@property
	def name(self):
		"""名前を返す
		
		Returns:
			str: 名前
		"""
		return self.__name

	@name.setter
	def name(self, val):
		"""名前を設定する
		
		Args:
			val(str): 名前
		"""
		self.__name = val
Python プログラム "RPG2.py" をご覧いただきたい。内容は "RPG1.py" とほぼ同じだが、まず、プレイヤー名のインスタンス変数を __name にすることで隠蔽した。
次に、隠蔽したインスタンス変数の二重アンダースコア __ を取り除いた名前のメソッド name を用意し、インスタンス変数 __name を返すだけの処理を書く。これをゲッターと呼び、メソッド定義の前に @property と書くことで、インスタンス名.name で呼び出すことができる。
また、同じメソッド名 name で、今度はインスタンス変数 __name に値を代入するメソッドを書く。そして、メソッド定義の前に @name.setter と書く。これをセッターと呼び、インスタンス名.name = 値 と書くことで値を代入できる。
このようなゲッター、セッターのことをプロパティと呼ぶ。
	def getParameters(self):
		"""パラメータをまとめて返す
		
		Returns:
			dict: パラメータ
		"""
		return self.__parameters

	@property
	def HP(self):
		"""HPを返す
		
		Args:
			key(str): 辞書キー
		
		Returns:
			dict: パラメータの値
		"""
		return self.__parameters["HP"]

	@HP.setter
	def HP(self, val):
		"""HPを設定する

		Args:
			val(int): 設定値
		
		Returns:
			None
		"""
		if (val < 0 or val > 99):
			raise ValueError("HPは0以上99以下で指定してください")
		self.__parameters["HP"] = val
パラメータ辞書のキー値 "HP" についても、同様にゲッターとセッターを用意した。
# メイン・プログラム =======================================================
# プレイヤーをリストに代入
players = [hero("ヒンメル"), wizard("フリーレン"), priest("ハイター"), warrior("アイゼン")]

print(f"players[0].name = {players[0].name}")
print(f"players[0].HP = {players[0].HP}")

try:
	# HPを+1する
	players[0].HP += 1
	print(f"players[0].HP = {players[0].HP}")
	# HPを100にする
	players[0].HP = 100
	print(f"players[0].HP = {players[0].HP}")
except ValueError as e:
	print(e)
"RPG2.py" を実行してみてほしい。プロパティを使って、ヒンメルの名前とHP値を表示する。
次に、HPを+1して表示する。最後に、HPに100を代入しようとするが、例外処理が走る。これは、セッターの中で、セットする値が0未満だったり99を超えていたりすると例外を発生するようにしてあるからだ。
プロパティの実体はメソッドなので、このようにバリデーションチェックを行うことができる。

ポリモーフィズム

"RPG3.py" を実行してみてほしい。設定した4人のプレイヤーの名前、職業、HPを順々に表示する。
# メイン・プログラム =======================================================
# プレイヤーをリストに代入
players = [hero("ヒンメル"), wizard("フリーレン"), priest("ハイター"), warrior("アイゼン")]

# ポリモーフィズムの例
for p in players:
	print(f"{p.name}の職業は{p.occupation},HPは{p.HP}")
クラスは "RPG2.py" と同じなので、メイン・プログラムだけ見ていこう。
for文 に注目してほしい。リスト players の要素は、それぞれ別の職業クラスからインスタンス化されたものだから、本来は異なるオブジェクトであるはずだ。
ところが Python では、異なるクラスやインスタンスでも、同じメソッドやプロパティを持っているなら、同じように扱うことができる。これをポリモーフィズムと呼ぶ。
今回のように1つの基底クラスを継承した派生クラスは、同じようなメソッドを備えていることが多いので、ポリモーフィズムを活用することで、オブジェクトによって条件分岐することなくシンプルなプログラムを書くことができる。

練習問題

次回予告

これは Python に限った話ではないが、ローカルドライブにあるファイルや、Webサイトやクラウドサービスとの入出力を行うプログラムをつくることは、自分の(組織の)資産を悪意のある第三者の目に止まるリスクにつながる。
次回は、これら入出力処理を学ぶ前に、セキュリティ対策の基本を押さえる。

コラム:ジェネレータ

フィボナッチ数列
「第6章 オブジェクト指向とクラス」の最後に、クラスに近い関数としてジェネレータを紹介する。例として、フィボナッチ数列を取り上げる。

フィボナッチ数列は、イタリアの数学者レオナルド・フィボナッチ(1170年頃~1250年(建長2年)頃)にちなんで名付けられた数列で、次の漸化式で表すことができる。
\[ \displaystyle
\left\{\begin{array}{ll}
F_0 = 0 \\
F_1 = 1 \\
F_{n+2} = F_n + F_{n+1} \quad (n \ge 1)
\end{array}\right. \]
フィボナッチ数列は、なぜか自然界によくあらわれる。たとえば、ヒマワリの種の並びや巻き貝の巻き方がフィボナッチ数列にしたがっていることが分かっている。美術で習う黄金比とも関係する。また、大学入試の数学で出題されることがあり、覚えている方も多いだろう。
def fibonacci1(n):
	"""フィボナッチ数列を求める(forループ版)
	
	Args:
		n(int): 求めたい数列の長さ
	
	Returns:
		list: フィボナッチ数列
		"""
	global Num
	fibList = [0, 1]		# 初期値
	for i in range(2, n):
		fibList.append(fibList[i - 2] + fibList[i - 1])
		Num += 1
	return fibList
フィボナッチ数列の漸化式を forループを使って実装したものがプログラム "Fibonacci1.py" のユーザー関数 fibonacci1 である。求めたい数列の長さを引数として渡し、フィボナッチ数列をリストで返す。20個のフィボナッチ数列を計算するのに18回の計算が必要になる。
def fibonacci2(i):
	"""フィボナッチ数列を求める(再帰呼び出し版)
	
	Args:
		i(int): 求めたい数列の順番
	
	Returns:
		list: フィボナッチ数列
		"""
	global Num
	if (i == 0):
		fib = 0
	elif (i == 1):
		fib = 1
	else:
		fib = fibonacci2(i - 1) + fibonacci2(i - 2)
	Num += 1

	return fib
# フィボナッチ数列を求める
fibList = []
for i in range(0, COUNT):
	fibList.append(fibonacci2(i))
フィボナッチ数列は漸化式で定義されているので、「5.3 再帰呼び出し」で学んだ手法を使って解くことができる。これがプログラム "Fibonacci2.py" のユーザー関数 fibonacci2 である。求めたい数列の順番を指定し、メイン・プログラム側で数列の数をコントロールできる
ところが(当然のことだが)、いちいち再帰を回すために計算回数は35,400回という非効率な結果になってしまった。
def fibonacci3():
	"""フィボナッチ数列を求める(再帰呼び出し)
	
	Returns:
		list: フィボナッチ数列
		"""
	global Num
	a, b = 0, 1
	while True:
		yield a
		a, b = b, a + b
		Num += 1
# フィボナッチ数列を求める
fibGenerator = fibonacci3()
fibList = []
for i in range(0, COUNT):
	fibList.append(next(fibGenerator))
forループ再帰呼び出しの中間形がジェネレータを用いたプログラム "Fibonacci3.py" である。求めたい数列の順番を指定し、メイン・プログラム側で数列の数をコントロールでき、計算回数は19回と forループより1回多いだけで済む。

ここで、fibonacci3 がジェネレータだ。defではじまる関数やメソッドの定義と同じだが、return文の代わりに yeild文がある。
yeild文は計算途中の値を返す。ここでは、while文を使った無限ループになっているが、yeild文を通る度に変数 a の値を呼び出し側に返し、そこで実行を停止する。そして、次に fibonacci3 が呼び出されたときは、実行を停止した場所から再開する。つまり、もう1回ループを回る。
ジェネレータの戻り値は generetaオブジェクトであり、この点でも関数とは異なる。次に呼び出すときは next(genere-taオブジェクト) のように next関数を使う。

このようにジェネレータは、数列や漸化式を計算するときに重宝する。
また、ジェネレータを使う場合、かならずしもリストを使う必要はなく、たとえば巨大なファイルを行単位で処理したい場合にメモリの消費を抑えることができる。
ジェネレータは、ループを何万回も回すようなシーンで、ループの途中の状態を拾うことが容易にできることから、無限級数の一部を抽出したり、無限ループに陥らないようチェックする時に利用できる。
(この項おわり)
header