Update 2023.11.19 2017.05.05

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

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

Java サンプルでは,金庫の見張りの同名作業の内容を,昼間と夜間という異なる状態により替えるというものである。

GoF の C++ サンプルでは,ネットワーク通信の同名応答を,異なる状態により替えるというものである。

Web で見つけた例は,ゲームのキャラクタの様々の状態があり,様々な同名局面の対応を替えるというものである。同名局面とは,想像がつくと思うが,「あるものを食べた」「敵に出会った」「穴に落ちた」等々である。自分の状態と同名局面の組み合わせにより対応を替えるのである。

オブジェクトの書き方の詳細は後述するが,Java サンプルでは(時計の比較以外)if 文がまったくなく,この発想は個人が独力で思いつくことが難しいと思われる。また,アルゴリズム?のミスがほとんどなくなるのではないかと思われる。

if文を使わず条件分岐した後に分岐の直後に戻るのも自動的に行われる。


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


◆◆State パターンとは◆◆

GoFによれば,State パターンの目的は, 「オブジェクトの内部状態が変化したときに,オブジェクトが振る舞いを変えるようにする。クラス内では,振る舞いの変化を記述せず,状態を表すオブジェクトを導入することでこれを実現する。」

GoFによれば,State パターンの別名は,Objects for States です。

GoFによれば,次に示すいずれかの場合に,State パターンを利用する。
・オブジェクトの振る舞いが状態に依存し,実行時にはオブジェクトがその状態により振る舞いを変えなければならない場合。
・オペレーションが,オブジェクトの状態に依存した多岐にわたる条件文を持っている場合。この状態はたいてい1つ以上の列挙型の定数で表されており,たびたび複数のオペレーションに同じ条件構造が現れる。State パターンでは,1つ1つの条件分岐を別々のクラスに受け持たせる。これにより,オブジェクトの各状態を1つのオブジェクトとして扱うことができるようになる。

GoFによれば,State パターンに関連するパターンは次のようなものである。
Flyweight パターン:いつどのように ConcreteState オブジェクトが共有されるのかを説明している。
Singleton パターン:ConcreteState オブジェクトは,しばしば Singleton パターンにあてはまる。

◆◆tkinter の実装◆◆

tkinter モジュールには大きく分けて,tkinter.Canvas クラスと tkinter.Frame クラスがあり,前者が自由なお絵かきソフトであり,後者がこのサンプルのようにパーツを配置するソフトです。このサンプルでは後者だけ使います。

ダイアログには,時計を表示するラベル,テキストボックス,そして,ボタンが5つあります。 tkinter では,もう一つ全体を囲むフレームを配置します。そして,ボタンの周りにもフレームがあります。

全体を囲むフレームの書き方に2つあります。1つはこのサンプルのように tkinter.Frame のサブクラスをつくりそれをメインルーチンでインスタンス化する方法です。パーツの配置はコンストラクタでやります。メインルーチンの書き方はこのサンプルが典型です。メインルーチンが tkinter の中でループしていますので本来のメインルーチンはコンストラクタに書きます。

もう1つの書き方は,普通のメソッドの中に全体を囲むフレームを tkinter.Tk() のように書きます。Web のサンプルはこちらの方が多いでしょう。フレームが2重にできるクセがあるようでして,withdraw()を使うようです(検証していません)。

各パーツを配置するのは簡単です。上から順に,各パーツのクラスをインスタンス化して,そのインスタンスを pack または grid するだけです。このサンプルでは前者だけを使います。各パーツの設定はインスタンス化のパラメータでやります。各パーツのパラメータ設定の詳細は他のWebサイトを参照してください。

この tkinter のポイントは,各パーツの状態が変化したときのイベントドリブンのパラメータの書き方です。このサンプルでは,押しボタンのイベントドリブンに command パラメータを使います。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
                 State                        Context
          (doClock, doUse,                 (setClock, changeState,
           doAlarm, doPhone)                 callSecurityCenter, recordLog)

           ↑             ↑                      ↑
        DayState         NightState           SafeFrame
(getInstance, do.)  (getInstance, do.)          (do.)
class State, class Context は,抽象メソッドでありテンプレートです。
class DayState, class NightState は,「昼間」「夜間」の状態で様々な作業をします。
class SafeFrame は,「昼間」「夜間」の状態に応じた対応します。tkinter でパーツを配置します。

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

◆このサンプルの動作

上に掲げたこのサンプルのダイアログを見てください。「金庫使用」「非常ベル」「通常通話」の3つのイベントがあります。このイベントに対する対応が「昼間」「夜間」では違っています。

どのように違うかはダイアログの中を見てください。非常ベルがなるといつでも通報します。昼間に金庫を使用すると記録だけですが,夜間に金庫を使用すると通報します。通常通話はいつでも録音だけです。記録の内容が違えば対応も違っているということです。「昼間」「夜間」の状態によりこれらの仕様を任意に替えられることがState パターンのポイントです。

このサンプルでは,tkinter と時計がありますので,本来,マルチスレッドで実装しなければならないのですが,PyScripter がバグりますので,時計は押しボタンで進めることにしました。State パターンの本質に問題ありません。この方が tkinter が安定します。

この実装の第1の特徴は if 文がないということです(時計の比較以外)。最後にある if は,自分がトップレベルにあるか判断するものです。オブジェクト指向を徹底すると if 文を減らせます。そうすると,アルゴリズムが簡単になりミスもなくなります。State パターンのオブジェクトの使い方は独力で思いつくようなものではないです。そのあたりを詳細に説明します。

class DayState と class NightState には同名メソッドがあり,「昼間」「夜間」の状態に合わせた対応をします。

if 文を使わないでこのような場合分けができるのは class DayState と class NightState のインスタンスが if 文の代わりをしているからです。これらのクラスは状態遷移ごとにインスタンス化されますのでメモリリークを防ぐため Singleton にします(Singleton パターンを参照,メモリリークについては Flyweight パターンを参照)。

◆昼間・夜間状態の切り替え

イベントドリブンのすべては class SafeFrame で発生します。Safe は「金庫」,Frame は「GUIのダイアログ」のことです。

Java サンプルでは GUIの時計は1時間毎にメソッド setClock を起動します。Python サンプルでは押しボタンで時計を進めてメソッド clock_plus, setClock, doClock を呼びます。そのとき昼間・夜間状態のインスタンスを前に付けて自分じしんのインスタンスを引数に入れて呼ぶのです。メソッド doClock は昼間・夜間状態の両方にありますが自動的に呼び分けられるのです。

呼び分けられたメソッド doClock は昼間・夜間状態を判定して,呼ばれたメソッドの引数にあったインスタンスを前に付けてメソッド changeState を呼ぶのです。呼ぶ方のメソッドの引数は昼間・夜間状態のインスタンスです。

メソッド changeState は昼間・夜間状態のインスタンスの切替を行います。

◆イベントドリブンの発生

Java サンプルでは GUIの押しボタンによりイベントドリブンが発生しメソッド actionPerformed が呼ばれます。Python サンプルで各パーツの command パラメータに書かれたメソッドを呼びます。このメソッドから昼間・夜間状態のメソッドである doUse, doAlarm, doPhone が呼び分けられます。呼び分けられる仕組みは時計の場合と同じです。

そして同じ仕組みでメソッド callSecurityCenter, recordLog が呼ばれて GUI のダイアログに掲示されます。

これらの自動的に呼び分けられる仕組みは,インスタンスをメソッドの前に付けることによりできます。そして自分じしんのインスタンスを引数に入れることにより自分のクラスに戻って来ることができるわけです。つまり,イベントドリブンは複数の場所にあってもよく呼んだところに帰って来るというわけです。

昼間・夜間状態を Singleton パターンにすることは必須ではありませんが,メモリリークを防ぐために Singleton パターンにした方が良いです(Java サンプルのとおり)。メモリリーク対策なければ,終日動作しているプログラムはクラッシュするのが必然となります。

◆◆ソースコード◆◆

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

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

【State】
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import tkinter.scrolledtext
import tkinter as Tk
from abc import ABCMeta, abstractmethod

class State(metaclass = ABCMeta):

    @abstractmethod
    def doClock(self, context, hour):           # 時刻設定
        pass
    @abstractmethod
    def doUse(self, context):                   # 金庫使用
        pass
    @abstractmethod
    def doAlarm(self, context):                 # 非常ベル
        pass
    @abstractmethod
    def doPhone(self, context):                 # 通常通話
        pass

class DayState(State):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "__instance__"):
            cls.__instance__ = super(DayState, cls).__new__(cls, *args, **kwargs)
        return cls.__instance__
    def getInstance():                          # 唯一のインスタンスを得る
        return DayState().__instance__
    def doClock(self, context, hour):           # 時刻設定
        if (hour < 9 or 17 <= hour):
            context.changeState(NightState.getInstance())
    def doUse(self, context):                   # 金庫使用
        context.recordLog("金庫使用(昼間)")
    def doAlarm(self, context):                 # 非常ベル
        context.callSecurityCenter("非常ベル(昼間)")
    def doPhone(self, context):                 # 通常通話
        context.callSecurityCenter("通常の通話(昼間)")
    def __str__(self):                  # インスタンスをprintすると呼ばれます
        return "[昼間]";

class NightState(State):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "__instance__"):
            cls.__instance__ = \
                super(NightState, cls).__new__(cls, *args, **kwargs)
        return cls.__instance__
    def getInstance():                          # 唯一のインスタンスを得る
        return NightState().__instance__
    def doClock(self, context, hour):           # 時刻設定
        if (9 <= hour and hour < 17):
            context.changeState(DayState.getInstance());
    def doUse(self, context):                   # 金庫使用
        context.callSecurityCenter("非常:夜間の金庫使用!");
    def doAlarm(self, context):                 # 非常ベル
        context.callSecurityCenter("非常ベル(夜間)");
    def doPhone(self, context):                 # 通常通話
        context.recordLog("夜間の通話録音");
    def __str__(self):                  # インスタンスをprintすると呼ばれます
        return "[夜間]";

class Context(metaclass = ABCMeta):

    @abstractmethod
    def setClock(self, hour):                   # 時刻の設定
        pass
    @abstractmethod
    def changeState(self, state):               # 状態変化
        pass
    @abstractmethod
    def callSecurityCenter(self, msg):          # 警備センター警備員呼び出し
        pass
    @abstractmethod
    def recordLog(self, msg):                   # 警備センター記録
        pass

class SafeFrame(Tk.Frame, Context):
    # コンストラクタ
    def __init__(self, master=None):
        # tkinter
        Tk.Frame.__init__(self, master, bg='light gray')
        self.master.title('State Sample')
        self.file_name=None
        # 時計
        self.Clock = Tk.StringVar()
        textClock = Tk.Label(
        self, textvariable=self.Clock, font=('MS ゴシック', 12),
            bg='light gray', width=20)
        textClock.pack()
        # 掲示板
        self.Screen = Tk.scrolledtext.ScrolledText(
            self, font=('MS ゴシック', 12), width=25, height=16)
        self.Screen.pack(fill=Tk.BOTH, expand=1)
        self.Screen.focus_set()
        # 押しボタン
        self.footer_area = Tk.Frame(self, bg='light gray')
        self.footer_area.pack()
        self.start_button = Tk.Button(
            self.footer_area, text="Clock+", bg='light gray', width=8,
            font=('Times', 10), command=self.clock_plus)
        self.start_button.pack(side=Tk.LEFT, padx=5,pady=5)
        self.a_button = Tk.Button(
            self.footer_area, text="金庫使用", bg='light gray', width=8,
            font=('Times', 10), command=self.buttonUse)
        self.a_button.pack(side=Tk.LEFT, padx=5,pady=5)
        self.b_button = Tk.Button(
            self.footer_area, text="非常ベル", bg='light gray', width=8,
            font=('Times', 10), command=self.buttonAlarm)
        self.b_button.pack(side=Tk.LEFT, padx=5,pady=5)
        self.c_button = Tk.Button(
            self.footer_area, text="通常通話", bg='light gray', width=8,
            font=('Times', 10), command=self.buttonPhone)
        self.c_button.pack(side=Tk.LEFT, padx=5,pady=5)
        self.d_button = Tk.Button(
            self.footer_area, text="終了", bg='light gray', width=8,
            font=('Times', 10), command=self.buttonExit)
        self.d_button.pack(side=Tk.LEFT, padx=5,pady=5)
        # tkinter終り
        # メインルーチン
        self.state = DayState.getInstance()     # 現在の状態
        self.hour = 22
        self.setClock(self.hour)                # 最初に,昼→夜
    # コンストラクタ終り
    def clock_plus(self):
        self.hour += 1
        if self.hour == 24:
            self.hour = 0
        self.setClock(self.hour)
    # ボタンが押されたらここに来る
    def buttonUse(self):
        #print("buttonUse") # debug
        self.state.doUse(self)
    def buttonAlarm(self):
        #print("buttonAlarm") # debug
        self.state.doAlarm(self)
    def buttonPhone(self):
        #print("buttonPhone") # debug
        self.state.doPhone(self)
    def buttonExit(self):
        print("sys.exit()") # bugります
    # 時刻の設定
    def setClock(self, hour):
        #self.hour = hour
        clockstring = "現在時刻は";
        if (hour < 10):
            clockstring += "0" + str(hour) + ":00";
        else:
            clockstring += str(hour) + ":00";
        #sys.stdout.write("{}\n".format(clockstring)) # debug
        self.Clock.set('ただいまの時間は {}:{}'.format(hour, "00"))
        self.state.doClock(self, hour)
    # 状態変化
    def changeState(self, state):
        #sys.stdout.write("{}から{}へ状態が変化しました。\n"
            #.format(self.state, state)) # debug
        self.Screen.insert(Tk.END, "{}から{}へ状態が変化しました。\n"
            .format(self.state, state))
        self.state = state
    # 警備センター警備員呼び出し
    def callSecurityCenter(self, msg):
        #sys.stdout.write("call! {}\n".format(msg)) # debug
        self.Screen.insert(Tk.END, "call! {}\n".format(msg))

    # 警備センター記録
    def recordLog(self, msg):
        #sys.stdout.write("record ... {}\n".format(msg)) # debug
        self.Screen.insert(Tk.END, "record ... {}\n".format(msg))

if __name__ == '__main__':
    app = SafeFrame()
    app.pack()
    app.mainloop()

以上

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