web-dev-qa-db-ja.com

Python Observerパターン:例、ヒント?

Pythonで実装されたGoF Observerの例示的な例はありますか?現在、キークラスを介して組み込まれているデバッグコードのビットが含まれているビットコードがあります(現在、マジックenvが設定されている場合、stderrにメッセージを生成します)。さらに、このクラスには、結果を段階的に返すためのインターフェイスと、結果を(メモリに)格納して後処理するためのインターフェイスがあります。 (クラス自体は、sshを介してリモートマシンでコマンドを同時に実行するためのジョブマネージャです)。

現在、クラスの使用法は次のようになります。

_job = SSHJobMan(hostlist, cmd)
job.start()
while not job.done():
    for each in job.poll():
        incrementally_process(job.results[each])
        time.sleep(0.2) # or other more useful work
post_process(job.results)
_

代替の使用モデルは次のとおりです。

_job = SSHJobMan(hostlist, cmd)
job.wait()  # implicitly performs a start()
process(job.results)
_

これはすべて、現在のユーティリティでは正常に機能します。ただし、柔軟性に欠けます。たとえば、私は現在、簡単な出力形式または漸進的な結果としての進行状況バーをサポートしています。また、post_process()関数の簡単で完全な「マージされたメッセージ」出力もサポートしています。

ただし、複数の結果/出力ストリーム(ターミナルへの進行状況バー、ログファイルへのデバッグと警告、成功したジョブから1つのファイル/ディレクトリへの出力、エラーメッセージ、および失敗したジョブから別の結果へのその他の結果)をサポートしたいと思います。 、など)。

これは、オブザーバーを呼び出す状況のように聞こえます...私のクラスのインスタンスに他のオブジェクトからの登録を受け入れさせ、発生したときに特定のタイプのイベントでコールバックします。

私は PyPubSub を参照しています。これは、SO関連する質問にいくつかの参照が含まれているためです。外部依存関係を自分に追加する準備ができているかどうかわかりませんユーティリティですが、他のユーザーがより簡単に使用できるようになれば、それらのインターフェイスを自分のモデルとして使用することに価値があると思います(このプロジェクトは、スタンドアロンのコマンドラインユーティリティと、他のスクリプト/ユーティリティを作成するためのクラスの両方として意図されています)。

要するに私は自分がやりたいことをする方法を知っています...しかし、それを達成するための多くの方法があります。長期的に見て、コードの他のユーザーにとって何が最も効果的であるかについての提案を求めています。

コード自体は、 classh にあります。

42
Jim Dennis

ただし、柔軟性に欠けます。

ええと...実際、非同期APIが必要な場合、これは私には良いデザインのように見えます。それは通常です。たぶん、必要なのはstderrからPythonの logging モジュールに切り替えることだけです。これには、独自のパブリッシュ/サブスクライブモデルがあり、Logger.addHandler()オン。

オブザーバーをサポートしたい場合は、シンプルにすることをお勧めします。ほんの数行のコードが必要です。

_class Event(object):
    pass

class Observable(object):
    def __init__(self):
        self.callbacks = []
    def subscribe(self, callback):
        self.callbacks.append(callback)
    def fire(self, **attrs):
        e = Event()
        e.source = self
        for k, v in attrs.iteritems():
            setattr(e, k, v)
        for fn in self.callbacks:
            fn(e)
_

JobクラスはObservableをサブクラス化できます。興味のあることが起こったら、self.fire(type="progress", percent=50)などを呼び出します。

48
Jason Orendorff

他の答えの人々はそれをやり過ぎると思います。 15行未満のコードでPython=のイベントを簡単に実現できます。

単純に2つのクラスがあります:EventObserver。イベントをリッスンするクラスは、オブザーバーを継承し、特定のイベントをリッスン(監視)するように設定する必要があります。 Eventがインスタンス化されて起動されると、そのイベントをリッスンするすべてのオブザーバーが指定されたコールバック関数を実行します。

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observables = {}
    def observe(self, event_name, callback):
        self._observables[event_name] = callback


class Event():
    def __init__(self, name, data, autofire = True):
        self.name = name
        self.data = data
        if autofire:
            self.fire()
    def fire(self):
        for observer in Observer._observers:
            if self.name in observer._observables:
                observer._observables[self.name](self.data)

class Room(Observer):

    def __init__(self):
        print("Room is ready.")
        Observer.__init__(self) # Observer's init needs to be called
    def someone_arrived(self, who):
        print(who + " has arrived!")

room = Room()
room.observe('someone arrived',  room.someone_arrived)

Event('someone arrived', 'Lenard')

出力:

Room is ready.
Lenard has arrived!
21
Pithikos

さらにいくつかのアプローチ...

例:ロギングモジュール

おそらく、stderrからPythonの logging モジュールに切り替えるだけで、強力なパブリッシュ/サブスクライブモデルがあります。

ログレコードの作成を開始するのは簡単です。

_# producer
import logging

log = logging.getLogger("myjobs")  # that's all the setup you need

class MyJob(object):
    def run(self):
        log.info("starting job")
        n = 10
        for i in range(n):
            log.info("%.1f%% done" % (100.0 * i / n))
        log.info("work complete")
_

消費者側ではもう少し仕事があります。残念ながら、ロガー出力を構成するには、7行のコードを実行する必要があります。 ;)

_# consumer
import myjobs, sys, logging

if user_wants_log_output:
    ch = logging.StreamHandler(sys.stderr)
    ch.setLevel(logging.INFO)
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    myjobs.log.addHandler(ch)
    myjobs.log.setLevel(logging.INFO)

myjobs.MyJob().run()
_

一方、loggingパッケージには驚くほど多くのものが含まれています。ログデータを一連のファイル、電子メールアドレス、およびWindowsイベントログに送信する必要がある場合は、問題ありません。

例:最も簡単なオブザーバー

ただし、ライブラリを使用する必要はまったくありません。オブザーバーをサポートする非常に簡単な方法は、何もしないメソッドを呼び出すことです。

_# producer
class MyJob(object):
    def on_progress(self, pct):
        """Called when progress is made. pct is the percent complete.
        By default this does nothing. The user may override this method
        or even just assign to it."""
        pass

    def run(self):
        n = 10
        for i in range(n):
            self.on_progress(100.0 * i / n)
        self.on_progress(100.0)

# consumer
import sys, myjobs
job = myjobs.MyJob()
job.on_progress = lambda pct: sys.stdout.write("%.1f%% done\n" % pct)
job.run()
_

ラムダを書く代わりに、_job.on_progress = progressBar.update_と言うこともできます。これはいいことです。

これは非常に簡単です。 1つの欠点は、同じイベントにサブスクライブする複数のリスナーを自然にはサポートしないことです。

例:C#のようなイベント

少しのサポートコードで、PythonでC#のようなイベントを取得できます。これがコードです:

_# glue code
class event(object):
    def __init__(self, func):
        self.__doc__ = func.__doc__
        self._key = ' ' + func.__name__
    def __get__(self, obj, cls):
        try:
            return obj.__dict__[self._key]
        except KeyError, exc:
            be = obj.__dict__[self._key] = boundevent()
            return be

class boundevent(object):
    def __init__(self):
        self._fns = []
    def __iadd__(self, fn):
        self._fns.append(fn)
        return self
    def __isub__(self, fn):
        self._fns.remove(fn)
        return self
    def __call__(self, *args, **kwargs):
        for f in self._fns[:]:
            f(*args, **kwargs)
_

プロデューサーは、デコレーターを使用してイベントを宣言します。

_# producer
class MyJob(object):
    @event
    def progress(pct):
        """Called when progress is made. pct is the percent complete."""

    def run(self):
        n = 10
        for i in range(n+1):
            self.progress(100.0 * i / n)

#consumer
import sys, myjobs
job = myjobs.MyJob()
job.progress += lambda pct: sys.stdout.write("%.1f%% done\n" % pct)
job.run()
_

これは上記の「単純なオブザーバー」コードとまったく同じように機能しますが、_+=_を使用して好きなだけリスナーを追加できます。 (C#とは異なり、イベントハンドラータイプはなく、イベントをサブスクライブするときにnew EventHandler(foo.bar)を実行する必要はありません。また、イベントを発生させる前にnullを確認する必要はありません。C#と同様に、イベントはスケルチ例外ではありません。)

選び方

loggingが必要なすべてを行う場合は、それを使用します。そうでなければ、あなたのために働く最も簡単なことをしてください。注意すべき重要な点は、大きな外部依存関係をとる必要がないことです。

13
Jason Orendorff

オブジェクトが何かを監視しているという理由だけでオブジェクトが存続しない実装についてはどうですか?以下に、以下の機能を備えたオブザーバーパターンの実装を示します。

  1. 使い方はPythonicです。インスタンスfooのバインドされたメソッド_.bar_にオブザーバーを追加するには、foo.bar.addObserver(observer)を実行するだけです。
  2. オブザーバーは、オブザーバーであることによって生かされていません。言い換えると、オブザーバーコードは強い参照を使用しません。
  3. サブクラス化は必要ありません(記述子ftw)。
  4. ハッシュ化できないタイプで使用できます。
  5. 1つのクラスで何度でも使用できます。
  6. (ボーナス)今日の時点で、コードは適切にダウンロード可能でインストール可能な github上のパッケージ に存在します。

コードは次のとおりです( githubパッケージ または PyPIパッケージ が最新の実装です):

_import weakref
import functools

class ObservableMethod(object):
    """
    A proxy for a bound method which can be observed.

    I behave like a bound method, but other bound methods can subscribe to be
    called whenever I am called.
    """

    def __init__(self, obj, func):
        self.func = func
        functools.update_wrapper(self, func)
        self.objectWeakRef = weakref.ref(obj)
        self.callbacks = {}  #observing object ID -> weak ref, methodNames

    def addObserver(self, boundMethod):
        """
        Register a bound method to observe this ObservableMethod.

        The observing method will be called whenever this ObservableMethod is
        called, and with the same arguments and keyword arguments. If a
        boundMethod has already been registered to as a callback, trying to add
        it again does nothing. In other words, there is no way to sign up an
        observer to be called back multiple times.
        """
        obj = boundMethod.__self__
        ID = id(obj)
        if ID in self.callbacks:
            s = self.callbacks[ID][1]
        else:
            wr = weakref.ref(obj, Cleanup(ID, self.callbacks))
            s = set()
            self.callbacks[ID] = (wr, s)
        s.add(boundMethod.__name__)

    def discardObserver(self, boundMethod):
        """
        Un-register a bound method.
        """
        obj = boundMethod.__self__
        if id(obj) in self.callbacks:
            self.callbacks[id(obj)][1].discard(boundMethod.__name__)

    def __call__(self, *arg, **kw):
        """
        Invoke the method which I proxy, and all of it's callbacks.

        The callbacks are called with the same *args and **kw as the main
        method.
        """
        result = self.func(self.objectWeakRef(), *arg, **kw)
        for ID in self.callbacks:
            wr, methodNames = self.callbacks[ID]
            obj = wr()
            for methodName in methodNames:
                getattr(obj, methodName)(*arg, **kw)
        return result

    @property
    def __self__(self):
        """
        Get a strong reference to the object owning this ObservableMethod

        This is needed so that ObservableMethod instances can observe other
        ObservableMethod instances.
        """
        return self.objectWeakRef()


class ObservableMethodDescriptor(object):

    def __init__(self, func):
        """
        To each instance of the class using this descriptor, I associate an
        ObservableMethod.
        """
        self.instances = {}  # Instance id -> (weak ref, Observablemethod)
        self._func = func

    def __get__(self, inst, cls):
        if inst is None:
            return self
        ID = id(inst)
        if ID in self.instances:
            wr, om = self.instances[ID]
            if not wr():
                msg = "Object id %d should have been cleaned up"%(ID,)
                raise RuntimeError(msg)
        else:
            wr = weakref.ref(inst, Cleanup(ID, self.instances))
            om = ObservableMethod(inst, self._func)
            self.instances[ID] = (wr, om)
        return om

    def __set__(self, inst, val):
        raise RuntimeError("Assigning to ObservableMethod not supported")


def event(func):
    return ObservableMethodDescriptor(func)


class Cleanup(object):
    """
    I manage remove elements from a dict whenever I'm called.

    Use me as a weakref.ref callback to remove an object's id from a dict
    when that object is garbage collected.
    """
    def __init__(self, key, d):
        self.key = key
        self.d = d

    def __call__(self, wr):
        del self.d[self.key]
_

これを使用するには、_@event_で監視可能にするメソッドを装飾するだけです。ここに例があります

_class Foo(object):
    def __init__(self, name):
        self.name = name

    @event
    def bar(self):
        print("%s called bar"%(self.name,))

    def baz(self):
        print("%s called baz"%(self.name,))

a = Foo('a')
b = Foo('b')
a.bar.addObserver(b.bar)
a.bar()
_
6
DanielSank

wikipedia から:

from collections import defaultdict

class Observable (defaultdict):

  def __init__ (self):
      defaultdict.__init__(self, object)

  def emit (self, *args):
      '''Pass parameters to all observers and update states.'''
      for subscriber in self:
          response = subscriber(*args)
          self[subscriber] = response

  def subscribe (self, subscriber):
      '''Add a new subscriber to self.'''
      self[subscriber]

  def stat (self):
      '''Return a Tuple containing the state of each observer.'''
      return Tuple(self.values())

Observableは次のように使用されます。

myObservable = Observable ()

# subscribe some inlined functions.
# myObservable[lambda x, y: x * y] would also work here.
myObservable.subscribe(lambda x, y: x * y)
myObservable.subscribe(lambda x, y: float(x) / y)
myObservable.subscribe(lambda x, y: x + y)
myObservable.subscribe(lambda x, y: x - y)

# emit parameters to each observer
myObservable.emit(6, 2)

# get updated values
myObservable.stat()         # returns: (8, 3.0, 4, 12)
4
Ewan Todd

Jasonの回答に基づいて、C#のようなイベントの例を本格的なpythonドキュメントとテストを含むモジュールとして実装しました。私は空想のPythonicが大好きです:)

したがって、すぐに使用できるソリューションが必要な場合は、 githubのコード を使用できます。

3
aepsil0n

例: ツイストログオブザーバー

オブザーバーyourCallable()(ディクショナリーを受け入れる呼び出し可能オブジェクト)を登録して、(他のオブザーバーに加えて)すべてのログイベントを受信するには:

twisted.python.log.addObserver(yourCallable)

例: 完全なプロデューサー/コンシューマーの例

Twisted-Pythonメーリングリストから:

#!/usr/bin/env python
"""Serve as a sample implementation of a twisted producer/consumer
system, with a simple TCP server which asks the user how many random
integers they want, and it sends the result set back to the user, one
result per line."""

import random

from zope.interface import implements
from twisted.internet import interfaces, reactor
from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver

class Producer:
    """Send back the requested number of random integers to the client."""
    implements(interfaces.IPushProducer)
    def __init__(self, proto, cnt):
        self._proto = proto
        self._goal = cnt
        self._produced = 0
        self._paused = False
    def pauseProducing(self):
        """When we've produced data too fast, pauseProducing() will be
called (reentrantly from within resumeProducing's transport.write
method, most likely), so set a flag that causes production to pause
temporarily."""
        self._paused = True
        print('pausing connection from %s' % (self._proto.transport.getPeer()))
    def resumeProducing(self):
        self._paused = False
        while not self._paused and self._produced < self._goal:
            next_int = random.randint(0, 10000)
            self._proto.transport.write('%d\r\n' % (next_int))
            self._produced += 1
        if self._produced == self._goal:
            self._proto.transport.unregisterProducer()
            self._proto.transport.loseConnection()
    def stopProducing(self):
        pass

class ServeRandom(LineReceiver):
    """Serve up random data."""
    def connectionMade(self):
        print('connection made from %s' % (self.transport.getPeer()))
        self.transport.write('how many random integers do you want?\r\n')
    def lineReceived(self, line):
        cnt = int(line.strip())
        producer = Producer(self, cnt)
        self.transport.registerProducer(producer, True)
        producer.resumeProducing()
    def connectionLost(self, reason):
        print('connection lost from %s' % (self.transport.getPeer()))
factory = Factory()
factory.protocol = ServeRandom
reactor.listenTCP(1234, factory)
print('listening on 1234...')
reactor.run()
2
jfs

オブザーバー設計への機能的アプローチ:

def add_listener(obj, method_name, listener):

    # Get any existing listeners
    listener_attr = method_name + '_listeners'
    listeners = getattr(obj, listener_attr, None)

    # If this is the first listener, then set up the method wrapper
    if not listeners:

        listeners = [listener]
        setattr(obj, listener_attr, listeners)

        # Get the object's method
        method = getattr(obj, method_name)

        @wraps(method)
        def method_wrapper(*args, **kwags):
            method(*args, **kwags)
            for l in listeners:
                l(obj, *args, **kwags) # Listener also has object argument

        # Replace the original method with the wrapper
        setattr(obj, method_name, method_wrapper)

    else:
        # Event is already set up, so just add another listener
        listeners.append(listener)


def remove_listener(obj, method_name, listener):

    # Get any existing listeners
    listener_attr = method_name + '_listeners'
    listeners = getattr(obj, listener_attr, None)

    if listeners:
        # Remove the listener
        next((listeners.pop(i)
              for i, l in enumerate(listeners)
              if l == listener),
             None)

        # If this was the last listener, then remove the method wrapper
        if not listeners:
            method = getattr(obj, method_name)
            delattr(obj, listener_attr)
            setattr(obj, method_name, method.__wrapped__)

これらのメソッドを使用して、任意のクラスメソッドにリスナーを追加できます。例えば:

class MyClass(object):

    def __init__(self, prop):
        self.prop = prop

    def some_method(self, num, string):
        print('method:', num, string)

def listener_method(obj, num, string):
    print('listener:', num, string, obj.prop)

my = MyClass('my_prop')

add_listener(my, 'some_method', listener_method)
my.some_method(42, 'with listener')

remove_listener(my, 'some_method', listener_method)
my.some_method(42, 'without listener')

そして出力は:

method: 42 with listener
listener: 42 with listener my_prop
method: 42 without listener
1
Dane White

OPは、「Pythonで実装されたGoF Observerの例示的な例はありますか?」と尋ねます。これはanの例Python 3.7です。このObservableクラスはone observablemanyオブザーバー残り独立彼らの構造。

from functools import partial
from dataclasses import dataclass, field
import sys
from typing import List, Callable


@dataclass
class Observable:
    observers: List[Callable] = field(default_factory=list)

    def register(self, observer: Callable):
        self.observers.append(observer)

    def deregister(self, observer: Callable):
        self.observers.remove(observer)

    def notify(self, *args, **kwargs):
        for observer in self.observers:
            observer(*args, **kwargs)


def usage_demo():
    observable = Observable()

    # Register two anonymous observers using lambda.
    observable.register(
        lambda *args, **kwargs: print(f'Observer 1 called with args={args}, kwargs={kwargs}'))
    observable.register(
        lambda *args, **kwargs: print(f'Observer 2 called with args={args}, kwargs={kwargs}'))

    # Create an observer function, register it, then deregister it.
    def callable_3():
        print('Observer 3 NOT called.')

    observable.register(callable_3)
    observable.deregister(callable_3)

    # Create a general purpose observer function and register four observers.
    def callable_x(*args, **kwargs):
        print(f'{args[0]} observer called with args={args}, kwargs={kwargs}')

    for gui_field in ['Form field 4', 'Form field 5', 'Form field 6', 'Form field 7']:
        observable.register(partial(callable_x, gui_field))

    observable.notify('test')


if __== '__main__':
    sys.exit(usage_demo())
0
lemi57ssss