Update 2023.11.17 2017.05.05

Python デザインパターン サンプルコード Observer
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。

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

Web で調べたところ Observer の別名はイベントリスナーと言われているらしい。一番近いと思われる別名は Publish-Subscribe 発行ー購読だそうだ。いずれにしても,GoF が言う目的を必要とするところに使われる。

通知を出す側は受け取り側が何をしているかをまったく関知しないように,密な結合ではなくて,それでいて受け取り損なうことのないようになっているのがObserver パターンです。

原著によれば,Observer パターンは,GUIプログラミングで広く用いられ,また,シミュレーションやサーバーといった,イベント処理を行う場面でも用いられる。そのほかにも,データベースのトリガー,Djangoのシグナル送受信システム,GUIアプリケーションフレームワークQtのシグナルとスロットのメカニズム,WebSocketなどで用いられるそうです。

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


◆◆Observer パターンとは◆◆

GoFによれば,Observer パターンの目的は, 「あるオブジェクトが状態を変えたときに,それに依存するすべてのオブジェクトに自動的にそのことが知らされ,また,それらが更新されるように,オブジェクト間に一対多の依存関係を定義する。」

GoFによれば,Observery パターンの別名は,Dependents,Publish-Subscribe パターンである。

GoFによれば,次のような状況で Observer パターンを使う。
・抽象化により,2つの面が,一方が他方に依存しているという形で現れる場合。これらの面をそれぞれ別々のオブジェクトにカプセル化することにより,それらを独立に変更したり,再利用することが可能になる。
・1つのオブジェクトを変化させるときに,それに伴いその他のオブジェクトも変化させる必要があり,しかも変化させる必要があるオブジェクトを固定的に決められない場合。
・オブジェクトが,他のオブジェクトに対して,それがどのようなものなのかを仮定せずに通知できるようにする場合。別の言い方をすると,これらのオブジェクトを密に結合したくない場合。

GoFによれば,Observer パターンの関連するパターンは次のようなものである。
Mediator パターン:複雑な更新のセマンティクスをカプセル化することにより,ChangeManagerオブジェクトは,subject と observer の間で mediator として働く。
Singleton パターン:ChangeManager オブジェクトは,自身を唯一の存在とし,グローバルにアクセスできるようにするために,Singleton パターンを使う。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
    Observed
    (observers_add,
    observer_discard,
    observers_notify)                  HistoryView    LiveView
      ↑                               (update)       (update)
    SliderModel
(value, minimum, maximum)
class Observed は,通知の出し側です。
class SliderModel は,観察対象です。
class HistoryView と class GraphObserver は,通知の受け取り側の具象クラスです。
class HistoryView は,タイムスタンプを表示します。
class LiveView は,バーグラフを表示します。

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

サンプルは,観察対象である class SliderModel が変化したときに class Observed が観察者に対して通知をだし,観察者である class HistoryView と class GraphObserver は,すべての通知を抜けが無いように受け取るようになっている。

【class LiveViewの出力】
                                        0
                                        7
                                        23
                                        37

【class HistoryViewの出力】

  0 2017-11-13 17:42:46.191359
  7 2017-11-13 17:42:46.191359
 23 2017-11-13 17:42:46.259939
 37 2017-11-13 17:42:46.259939

class SliderModel は,観察対象です。サンプルなので何でも良いのですが,ある値の最小値,最大値,現在値を保持します。

このクラスのインスタンスメソッドは,次の同じ名称のものが3組あります。
  def value
  def minimum
  def maximum

その内の1組のコードは次のようになっています。

    @property
    def value(self):
        return self.__value

    @value.setter
    def value(self, value):
        if self.__value != value:
            self.__value = value
            self.observers_notify()

これらは,いわゆる getter, setter をデコレータで表現したものです。デコレータを使わなければ次のように書くのが普通です。
  def get_value
  def set_value

さて,setter が呼ばれたとき,スーパーclass Observed のメソッド observers_notify を呼んでいます。さらに,観察者のメソッド update を呼んでいます。

実は,通知を出すスーパーclass Observed は,観察者が誰であるかを関知していません。メインルーチンがこのクラスのメソッドを使って,観察者のインスタンスの set を生成しているのです。スーパーclass Observed は,自分のインスタンス変数から観察者のインスタンスをアンパックして通知を出しているのです。

観察者の方は,自分のメソッドが呼ばれていますので,通知を抜けなく受け取ることができます。観察者が何をしているかは,出力から判るとおもいますので,詳細は省略します。

サンプルの観察対象は単純ですが,実際の応用では,観察対象の変化はよりランダムに起こると思われますが,通知は抜けなく受け取ることができます。

◆◆ソースコード◆◆

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

ソースファイルは1つです。サンプル出力を添付します。
・observer.py;Observer パターンのPythonサンプル

【observer.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
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import datetime
import itertools
import sys
import time


def main():
    historyView = HistoryView()
    liveView = LiveView()
    model = SliderModel(0, 0, 40) # minimum, value, maximum
    model.observers_add(historyView, liveView)  # liveView produces output
    for value in (7, 23, 37):
        model.value = value                     # liveView produces output
    for value, timestamp in historyView.data:
        print("{:3} {}".format(value, datetime.datetime.fromtimestamp(
                timestamp)), file=sys.stderr)


class Observed:

    def __init__(self):
        self.__observers = set()


    def observers_add(self, observer, *observers):
        for observer in itertools.chain((observer,), observers):
            self.__observers.add(observer)
            observer.update(self)


    def observer_discard(self, observer):
        self.__observers.discard(observer)


    def observers_notify(self):
        for observer in self.__observers:
            observer.update(self)


class SliderModel(Observed):

    def __init__(self, minimum, value, maximum):
        super().__init__()
        # These must exist before using their property setters
        self.__minimum = self.__value = self.__maximum = None
        self.minimum = minimum
        self.value = value
        self.maximum = maximum


    @property
    def value(self):
        return self.__value


    @value.setter
    def value(self, value):
        if self.__value != value:
            self.__value = value
            self.observers_notify()


    @property
    def minimum(self):
        return self.__minimum


    @minimum.setter
    def minimum(self, value):
        if self.__minimum != value:
            self.__minimum = value
            self.observers_notify()


    @property
    def maximum(self):
        return self.__maximum


    @maximum.setter
    def maximum(self, value):
        if self.__maximum != value:
            self.__maximum = value
            self.observers_notify()


class HistoryView:

    def __init__(self):
        self.data = []


    def update(self, model):
        self.data.append((model.value, time.time()))


class LiveView:

    def __init__(self, length=40):
        self.length = length


    def update(self, model):
        tippingPoint = round(model.value * self.length /
                (model.maximum - model.minimum))
        td = '<td style="background-color: {}">&nbsp;</td>'
        html = ['<table style="font-family: monospace" border="0"><tr>']
        html.extend(td.format("darkblue") * tippingPoint)
        html.extend(td.format("cyan") * (self.length - tippingPoint))
        html.append("<td>{}</td></tr></table>".format(model.value))
        print("".join(html))


if __name__ == "__main__":
    main()

以上

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