Update 2023.11.18 2017.05.05

Python デザインパターン サンプルコード Strategy
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

◆◆Strategy パターンの使われる場面◆◆

Strategy戦略のアルゴリズムが複数ありかつ複雑な場合,Strategy戦略を独立させ交換可能にするのがStrategy パターンである。Strategy戦略をクライアントから隠蔽することもできる。


(2023-11-18)Python3.11で動作確認済み

【重要な注意】本ソースコードファイルを起動するには第2引数が必要です。ターミナルからPythonを起動するとき,普通はプロンプト「'>'」の後に次のようにタイプします。

>python Strategy.py 314 15(ケースバイケースで)

ターミナルによっては'python'は'python3'になります。ソースコードファイル(スクリプトファイル)名称が第1引数です。第2引数は本プログラムに読み込むオプションです。

「PyCharm」では,次の枠に第2引数を設定します。
  メニュー→run→Edit Configurations...→左下矢印と右上矢印の枠
  ファイル名→Modify Run Configuration...→左下矢印と右上矢印の枠
  画面によっては,Interpreter options: という枠です(左下矢印と右上矢印の枠)

引数が必要なところに「sys.argv[1]」を入れます。「Jupyter Notebook」では,自己テストに「sys.argv[1]=???」とする場合があります。そのときは「PyCharm」では,この行は必要ありません。「Jupyter Notebook」にこの行を入れるのは,ハードコードと言い,固有名詞や固有な数値をコマンドラインに埋め込むことは本来避けるべきですが,この場合はやむを得ないとしましょう。ちなみにこのコードでは第1引数のような扱いになっていますが,ターミナルのコマンドラインでは第2引数なので混乱しないように。

「sys.argv[1]」の前かまたは冒頭に「import sys」を忘れないように。

.pyではターミナルから実行されたとき自己テストが実行され,他のファイルから呼ばれたときは自己テストは無視されることも忘れないように。「Jupyter Notebook」では呼ばれる側を上側に置き,下側に参照されるようにします(本記事に関係ないかも)。


◆◆Strategy パターンとは◆◆

GoFによれば,Strategy パターンの目的は, 「アルゴリズムの集合を定義し,各アルゴリズムをカプセル化して,それらを交換可能にする。Strategy パターンを利用することで,アルゴリズムを,それを利用するクライアントからは独立に変更することができるようになる。」

GoFによれば,Strategy パターンの別名は,Policy パターンである。

GoFによれば,Strategy パターンは次のような場合に利用する。
・関連する多くのクラスが振る舞いのみ異なっている場合。Strategy パターンは,多くの振る舞いの中の1つでクラスを構成する方法を提供する。
・複数の異なるアルゴリズムを必要とする場合。たとえば,空間と時間のトレードオフを反映する複数のアルゴリズムを定義する場合が考えられる。このとき,複数のアルゴリズムをクラス階層[HO87]として実装していく際に,Strategy パターンを利用することができる。
・アルゴリズムが,クライアントが知るべきではないデータを利用している場合。Strategy パターンを利用することにより,複雑でアルゴリズムに特有なデータ構造を公開するのを避けることができる。
・クラスが多くの振る舞いを定義しており,これらがオペレーション内で複数の条件文として現れている場合。このとき,多くの条件文を利用する代わりに,条件分岐後の処理を Strategy クラスに移し換える。

GoFによれば,Strategy パターンに関連するパターンは次のようなものである。
Flyweight パターン: ConcreteStrategy オブジェクトは,しばしば有効な flyweight になる。

◆◆Strategy パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
Hand               Player                          Strategy
(get Hand,        (nextHand,                   (nextHand, study)
 isStrongerThan,   win                         ↑              ↑
 isWeakerThan,     loose             WinningStrategy        ProbStrategy
 fight,            even           (nextHand, study)    (nextHand, getSum, study)
 toString)         toString)
サンプルはジャンケンの試合です。

class Hand は,試合のルールです。
class Player は,試合の進行です。審判の役割もしています。
class Strategy は,戦略のテンプレートであり,抽象クラスです。Java サンプルでは interface になっています。
class WinningStrategy と class ProbStrategy は,戦略の具象クラスです。これらは独立していて取替が可能です。ジャンケンの次の手をいろいろな戦略により決めることがこのクラスの役割です。class WinningStrategy はいわゆる愚かな戦略であり,class ProbStrategy はいわゆる賢い戦略です。
クライアント;メインルーチンは観戦というか事務局みたいなものです。

◆◆Strategy パターンの実装◆◆

◆class Hand は試合のルール

ジャンケンの手を後で計算し易いように整数 0, 1, 2 に置き換えています。Java サンプルでは static であるのでクラス定数となります(インスタンス間で共通)。 試合の勝負を決定するのがこのクラスの役割です。

◆class Player は試合の進行

試合数や勝ち数や負け数をカウントしています。

◆WinningStrategy はいわゆる愚かな戦略

このサンプルではコマンドラインの引数に乱数発生の seed を2つ書きます。メインルーチンで class Player をインスタンス化すると同時に2つの戦略の具象クラスを,コマンドラインの引数 sys.argv[1], sys.argv[2] をパラメータにしてインスタンス化していてそれらをコンストラクタで読み込んでいます。

ちなみにPython では sys.argv[0] は起動するモジュール名であり Java とは違います。また,Javaサンプルでは seed をパラメータにしてRandom クラスをインタンス化していて乱数発生のときにそのRandom 型のインスタンスを使っています。Java の乱数発生の仕組みがそうなっているということです。

いずれにしても,seed に定数を入れるので乱数は何回やっても同じものになります。この方がデバッグはやり易くなります。繰り返さない乱数がほしいときは seed に何も入力しないでください。

さて,愚かな戦略とは,勝ったときの次の手は同じ手を出し,おあいこと負けたときの次の手はデタラメの手を出すというものです。

次の手を決めるときに class Hand のインスタンスを使わないで直接メソッド getHand を呼んでいますが,class Hand のコンストラクタがパラメータを使っているので,class Hand を直接呼ぶときもパラメータが必要となります。このパラメータはジャンケンの手の変数ですがダミーを入れても問題はありません。Java ではパラメータは必要ないようです。

◆ProbStrategy はいわゆる賢い戦略

賢い戦略とは,勝ったときの手とその前の手の組合せを記録しておいて確率計算に利用します。例えば,グーの次の手グー・チョキ・パーが勝った回数を2, 4, 6回とすると,2/12, 4/12, 6/12の確率で次の手を決めます。

具体的な方法は,勝った回数の合計の個数の整数を乱数発生で出力させ,0,1ならグー,2,3,4,5ならチョキ,6,7,8,9,10,11ならパーを出すことにします。

この戦略は,回数が多くないと確率に差がでてこないという欠点があります。また,勝ったとき相手はデタラメの手を出すので確率の意味が薄くなります。この戦略は,相手にクセがあるほど効果がでます。相手は勝ったときの次の手はかならず同じになるクセがあるのでこのときだけこちらの勝つ確率が上がります。

コメントアウトしてある print はデバッグに使ったものです。愚かな戦略の検証はアルゴリズムどおりになっているかを確認しました。賢い戦略の検証は愚かな方が同じ手を出し続けるというクセをシミュレーションすると圧倒的に勝つということで確認できます。

◆◆ソースコード◆◆

このWebの左上隅からダウンロードできます。

ソースファイルは1つです。
・Strategy.py;Strategy パターンのPythonサンプル

【Strategy.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import random
from abc import ABCMeta, abstractmethod

class Hand():
    HANDVALUE_GUU = 0
    HANDVALUE_CHO = 1
    HANDVALUE_PAA = 2
    hand = [HANDVALUE_GUU, HANDVALUE_CHO, HANDVALUE_PAA]
    name = ["グー", "チョキ", "パー"]
    def __init__(self, handvalue):
        self.handvalue = handvalue
    def getHand(self, handvalue):
        return self.hand[handvalue]
    def isStrongerThan(self, h):
        return self.fight(h) == 1
    def isWeakerThan(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 toString(self):
        return self.name[self.handvalue]

class Strategy(metaclass=ABCMeta):
    @abstractmethod
    def nextHand():
        pass
    @abstractmethod
    def study(win):
        pass

class WinningStrategy(Strategy):
    won = False
    prevHand = 0
    def __init__(self, _seed):
        random.seed(_seed)
    def nextHand(self):
        if not(self.won):
            self.prevHand = Hand(3).getHand(random.randint(0, 2))
        return self.prevHand
    def study(self, win):
        self.won = win

class ProbStrategy(Strategy):
    prevHandValue = 0
    currentHandValue = 0
    history = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
    def __init__(self, _seed):
        random.seed(_seed)
    def nextHand(self):
        bet = random.randint(0, self.getSum(self.currentHandValue))
        handvalue = 0
        if bet < self.history[self.currentHandValue][0]:
            handvalue = 0
        elif bet < self.history[self.currentHandValue][0] + \
                self.history[self.currentHandValue][1]:
            handvalue = 1
        else:
            handvalue = 2
        self.prevHandValue = self.currentHandValue
        self.currentHandValue = handvalue
        return Hand(3).getHand(handvalue)
    def getSum(self, hv):
        _sum = 0
        for i in range(3):
            _sum += self.history[hv][i]
        return _sum
    def study(self, win):
        if win:
            self.history[self.prevHandValue][self.currentHandValue] += 1
        else:
            self.history[
                self.prevHandValue][(self.currentHandValue + 1) % 3] += 1
            self.history[
                self.prevHandValue][(self.currentHandValue + 2) % 3] += 1

class Player():
    wincount = 0
    losecount = 0
    gamecount = 0
    def __init__(self, name, strategy):
        self.name = name
        self.strategy = strategy
    def nextHand(self):
        return self.strategy.nextHand()
    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 toStirng(self):
        return "[{0}: {1} games {2} win {3} lose]" \
            .format(self.name, self.gamecount,self.wincount, self.losecount)

def main():
    if len(sys.argv) != 3:
        sys.stdout.write(
            "Usage: python Strategy.py randomseed1 randomseed2\n")
        sys.stdout.write("Example: python Strategy.py 314 15")
        sys.exit()
    seed1 = int(sys.argv[1])
    seed2 = int(sys.argv[2])
    player1 = Player("Taro", WinningStrategy(seed1))
    player2 = Player("Hana", ProbStrategy(seed2))
    for i in range(10):         # turn, ex;10000
        x = player1.nextHand()
        #x = 2
        nextHand1 = Hand(x)
        #print(x)
        y = player2.nextHand()
        nextHand2 = Hand(y)
        #print(y)
        if nextHand1.isStrongerThan(nextHand2):
            sys.stdout.write("Winner : {0}\n".format(player1.toStirng()))
            player1.win()
            player2.lose()
        elif nextHand2.isStrongerThan(nextHand1):
            sys.stdout.write("Winner : {0}\n".format(player2.toStirng()))
            player1.lose()
            player2.win()
        else:
            sys.stdout.write("Even ...\n")
            player1.even()
            player2.even()

    sys.stdout.write("Total result:\n")
    sys.stdout.write(player1.toStirng())
    sys.stdout.write("\n")
    sys.stdout.write(player2.toStirng())

if __name__ == "__main__":
    main()
"""
コマンドライン : 314 15
Even ...
Winner : [Taro: 1 games 0 win 0 lose]
Winner : [Hana: 2 games 0 win 1 lose]
Even ...
Winner : [Hana: 4 games 1 win 1 lose]
Winner : [Hana: 5 games 2 win 1 lose]
Even ...
Even ...
Winner : [Taro: 8 games 1 win 3 lose]
Even ...
Total result:
[Taro: 10 games 2 win 3 lose]
[Hana: 10 games 3 win 2 lose]
"""

以上

トップページに戻る
inserted by FC2 system