Python
デザインパターン
DesignPatterns

デザインパターン(Design Pattern)#Strategy

More than 3 years have passed since last update.

設計を意識したコードが書けるようになる為に、デザインパターン修行しました。

他のDesign Patternもちょくちょく出していきます。


前置き

デザインパターンをどういう時に、何を、どう使うのかを理解することが一先ずの目標。

(Javaというか静的型付言語は初めてで、且つpython歴もそんなに長くないので、Pythonistaぽっくないところがあると思います。ご指摘ございましたらご教授ください。)

今回は、振る舞いに関するパターンStrategy。


Strategyとは

アルゴリズムを実装した部分がごっそりと交換できるようになっています。アルゴリズムを切り替え、同じ問題を別の方法で解くのを容易にするパターン。

それは、Strategyパターンでは、アルゴリズムの部分を他の部分と意識的に分離しているので。そしてアルゴリズムとのインタフェース(API)の部分だけを規定します。そして、プログラムから委譲によってアルゴリズムを利用する。委譲というゆるやかな結びつきを使うことによって、アルゴリズムを容易に切り替えることができます。

多くの振る舞いが 複数の条件文として現れている場合にStrategyパターンが使えるでしょう。


概要

ここで作るサンプルプログラムは、コンピュータで「じゃんけん」を行うものです。

じゃんけんの「戦略」として、2つの方法を考えました。1つは「勝ったら次も同じ手を出す」という方法(WinningStrategy)、もう1つは「1回前の手から次の手を確率的に計算する」という方法(ProbStrategy)です。


全体のクラス図


hand.py

class Hand():

HANDVALUE_ROCK = 0
HANDVALUE_SCISSORS = 1
HANDVALUE_PAPER = 2
NAMES = ['グー', 'チョキ', 'パー']
HANDS = [HANDVALUE_ROCK,
HANDVALUE_SCISSORS,
HANDVALUE_PAPER]

def __init__(self, handvalue):
self.__handvalue = handvalue

def get_hand(self, handvalue):
return self.HANDS[self.__handvalue]

def is_stronger_than(self, h):
return self.__fight(h) == 1

def is_weaker_than(self, h):
return self.__fight(h) == -1

def __fight(self, h):
if self.__handvalue == h.__handvalue:
return 0
elif (self.__handvalue + 1) % 3 == h.__handvalue:
return 1
else:
return -1

def to_string(self):
return self.NAMES[self.__handvalue]


Handクラスはじゃんけんの「手」を表すクラスです。クラスの内部ではグーは0、チョキは1、パーは2という表現をしています。これを手の値を表すフィールド(handvalue)で保存します。

Handクラスのインスタンスは3つしか作られません。最初に3つのインスタンスが作られ、配列のHANDSに保存されています。

クラスメソッドget_handを使って、インスタンスを得ることが出来ます。手の値を引数で与えると、インスタンスが戻り値となります。

is_stronger_thanとis_weaker_thanは手の強さを比較するものです。2つの手hand1とhand2がある時に使います。

このクラスの内部で実際に手の強さを判定しているのは、fightというメソッドです。強さの判定では手の値を使っています。ここで使われている(self.__handvalue + 1) % 3 == h.__handvalue はselfの手の値に1を加えたものがhの手の値(selfがグーならhはチョキ、selfがチョキならhはパー、selfがパーならhはグー)になっているなら、selfはhより強いということになります。演算子%を使って3の剰余を取ってるのは、パー(2)に1を加えたときに、グー(0)になって欲しいからです。



strategy.py

from abc import ABCMeta, abstractmethod

class Strategy(metaclass=ABCMeta):

@abstractmethod
def next_hand():
pass

@abstractmethod
def study(win):
pass


Strategyインターフェースは、じゃんけんの「戦略」のための抽象メソッドを集めたものです。

next_handは「次に出す手を得る」ためのメソッドです。このメソッドが呼ばれると、Strategyインタフェースを実装するクラスは「次の一手」を決めます。

studyは、「さっき出した手によって勝ったかどうか」を学習するためのメソッドです。直前のnext_handメソッド呼び出しで勝った場合には、study(True)として呼び出します。負けた場合には、study(False)として呼び出します。これらによって、Strategyインタフェースを実装するクラスは、自分の内部状態を変化させ、次回以降のnext_handメソッドの戻り値を決定する材料とするのです。



winning_strategy.py

import random

from hand import Hand
from strategy import Strategy

class WinningStrategy(Strategy):

__won = False
__prev_hand = 0

def __init__(self, seed):
self.__rand = seed

def next_hand(self):
if not(self.__won):
self.__prev_hand = Hand(self.__rand).get_hand(random.randint(0, 3))
return self.__prev_hand

def study(self, win):
self.__won = win


WinningStrategyクラスは、Strategyインタフェースを実装するクラスの1つです。Strategyインタフェースを実装しているということは、next_handとstudyという2つのメソッドを実装していることになります。

このクラスは、前回の勝負に勝ったならば、次も同じ手(グーならグー、パーならパー)を出す戦略をとります。もしも前回の勝負に負けたらなば、次の手は乱数を使って決定します。

randフィールドは、このクラスが乱数を必要とするときに使う乱数を保持します。

wonフィールドは、前回勝負の結果を保持します。勝ったならTrue、負けたならFalseになります。

prev_handフィールドは、前回の勝負で出した手を保持します。


prob_strategy.py

import random

from hand import Hand
from strategy import Strategy

class ProbStrategy(Strategy):

__prev_hand_value = 0
__current_hand_value = 0
__history = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]

def __init__(self, seed):
self.__rand = seed

def next_hand(self):
bet = random.randint(0, self.__get_sum(self.__current_hand_value))
hand_value = 0
if bet < self.__history[self.__current_hand_value][0]:
hand_value = 0
elif bet < self.__history[self.__current_hand_value][0] + \
self.__history[self.__current_hand_value][1]:
hand_value = 1
else:
hand_value = 2

self.__prev_hand_value = self.__current_hand_value
self.__current_hand_value = hand_value

return Hand(hand_value).get_hand(hand_value)

def __get_sum(self, hv):
total = 0
for i in range(0, 3):
total += self.__history[hv][i]
return total

def study(self, win):
if win:
self.__history[self.__prev_hand_value][self.__current_hand_value] \
+= 1
else:
self.__history[self.__prev_hand_value][(self.__current_hand_value + 1) % 3] \
+= 1
self.__history[self.__prev_hand_value][(self.__current_hand_value + 2) % 3] \
+= 1


ProbStrategyクラスは、もう1つの具体的な「戦略」です。次の手はいつも乱数で決定するのですが、過去の勝ち負けの履歴を使って、それぞれの手を出す確立を変えています。

historyフィールドが、過去の勝敗を反映した確率計算のための表になっています。

historyはintegerの2次元配列で、各次元の添え字は次のような意味を持ちます。

history[前回に出した手][今回出す手]

この式の値が大きければ大きいほど、過去の勝率が高い、ということになります。

詳しく書くと。

history[0][0] グー、グーと自分が出したときの過去の勝ち数

history[0][1] グー、チョキと自分が出したときの過去の勝ち数

history[0][2] グー、パーと自分が出したときの過去の勝ち数

前回、自分がグーを出したとします。そのとき、次に自分が何を出すかを、上記のhistroy[0][0]、histroy[0][1]、histroy[0][2]の値から確率で計算してます。要するに、この3つの式の値を足し(get_sumメソッド)、0からその数を計算し、そしてそれを元に次の手を決めます(next_handメソッド)

例えば、

history[0][0]の値が3

history[0][1]の値が5

history[0][2]の値が7

の場合。この時、グー、チョキ、パーを出す割合を3:5:7として次の手を決めます。0以上15未満(15は3+5+7の値)の乱数値を得て、

0以上3未満ならグー

3以上8未満ならチョキ

8以上15未満ならパー

とします。

studyメソッドは、next_handメソッドで返した手の勝敗を元に、historyフィールドの内容を更新します。



player.py

class Player():

__wincount = 0
__losecount = 0
__gamecount = 0

def __init__(self, name, strategy):
self.__name = name
self.__strategy = strategy

def next_hand(self):
return self.__strategy.next_hand()

def win(self):
self.__strategy.study(True)
self.__wincount += 1
self.__gamecount += 1

def lose(self):
self.__strategy.study(False)
self.__losecount += 1
self.__gamecount += 1

def even(self):
self.__gamecount += 1

def to_stirng(self):
return '[{0}: {1} games {2} win {3} lose]'.format(self.__name,
self.__gamecount,
self.__wincount,
self.__losecount)


Playerクラスは、じゃんけんを行う人を表すクラスです。

Playerクラスは「名前」と「戦略」を与えられてインスタンスを作ります。

next_handメソッドは次の手を得るためのものですが、実際に次の手を決定するのは自分の「戦略」です。戦略のnext_handメソッドの戻り値が、そのままPlayerのnext_handメソッドの戻り値となります。next_handメソッドは、自分の行うべき処理をStrategyに「委譲」しています。

勝ったり(win)、負けたり(lose)、引き分けたり(even)した勝負の結果を次回の勝負に生かすために、Playerクラスは、startegyフィールドを通してstudyメソッドを呼び出します。studyメソッドを使って戦略の内部状態を変化させるます。wincount、losecount、geamecountは、プレイヤーの勝ち数を記録するものです。


main.py

import random

import sys
from winning_strategy import WinningStrategy
from prob_strategy import ProbStrategy
from player import Player
from hand import Hand

def main():
try:
if int(sys.argv[1]) >= 3:
seed1 = random.randint(0, 2)
else:
seed1 = int(sys.argv[1])

if int(sys.argv[2]) >= 3:
seed2 = random.randint(0, 2)
else:
seed2 = int(sys.argv[2])

player1 = Player('Taro', WinningStrategy(seed1))
player2 = Player('Hana', ProbStrategy(seed2))
for i in range(0, 10): # 10000
next_hand1 = Hand(player1.next_hand())
next_hand2 = Hand(player2.next_hand())
if next_hand1.is_stronger_than(next_hand2):
print('Winner : {0}'.format(player1.to_stirng()))
player1.win()
player2.lose()
elif next_hand2.is_stronger_than(next_hand1):
print('Winner : {0}'.format(player2.to_stirng()))
player1.lose()
player2.win()
else:
print('Even ...')
player1.even()
player2.even()

print('Total result:')
print(player1.to_stirng())
print(player2.to_stirng())

except IndexError:
print('Check args size, does not work')
print('usage: python main random_seed1 random_seed2')
print('Example: python main.py 314 15')

if __name__ == "__main__":
main()


実行結果

python main.py 21  3

Winner : [Hana: 0 games 0 win 0 lose]
Even ...
Winner : [Hana: 2 games 1 win 0 lose]
Winner : [Taro: 3 games 0 win 2 lose]
Even ...
Winner : [Taro: 5 games 1 win 2 lose]
Even ...
Winner : [Hana: 7 games 2 win 2 lose]
Winner : [Taro: 8 games 2 win 3 lose]
Winner : [Hana: 9 games 3 win 3 lose]
Total result:
[Taro: 10 games 3 win 4 lose]
[Hana: 10 games 4 win 3 lose]


まとめ

Strategyパターンは、アルゴリズムの部分を他の部分と意識的に分離します。委譲というゆるやかな結びつきを使っているので、インタフェース部分を変更しないようにすれば、アルゴリズムの切り替えが簡単にできます。


参考