Pythonのイベントシステム


195

Pythonのどのイベントシステムを使用していますか?私は既にpydispatcherを知っていますが、他に何が見つかるか、一般的に使用されているのかと思っていましたか?

大規模なフレームワークの一部であるイベントマネージャーには興味がありません。簡単に拡張できる小さな最小限のソリューションを使用したいと思います。

回答:


178

PyPIパッケージ

2020年6月現在、これらはPyPIで利用可能なイベント関連パッケージであり、最新のリリース日順に並べられています。

もっとあります

これは、非常に異なる用語(イベント、シグナル、ハンドラー、メソッドディスパッチ、フックなど)を使用して選択できる多くのライブラリです。

上記のパッケージの概要に加えて、ここで回答に記載されている手法を維持しようとしています。

まず、いくつかの用語...

オブザーバーパターン

イベントシステムの最も基本的なスタイルは、オブザーバーパターンの単純な実装である「バッグオブハンドラーメソッド」です。

基本的に、ハンドラーメソッド(呼び出し可能オブジェクト)は配列に格納され、イベントが発生したときにそれぞれ呼び出されます。

パブリッシュサブスクライブ

Observerイベントシステムの欠点は、実際のイベントオブジェクト(またはハンドラーリスト)にしかハンドラーを登録できないことです。したがって、登録時にイベントがすでに存在している必要があります。

そのため、イベントシステムの2番目のスタイルであるpublish-subscribeパターンが存在し ます。ここで、ハンドラーはイベントオブジェクト(またはハンドラーリスト)ではなく、中央のディスパッチャーに登録します。また、通知機能はディスパッチャとのみ通信します。何をリッスンするか、何をパブリッシュするかは、名前(文字列)以外の「シグナル」によって決定されます。

メディエーターパターン

同様に興味があるかもしれません:Mediatorパターン

フック

「フック」システムは、アプリケーションプラグインのコンテキストで通常使用されます。アプリケーションには固定の統合ポイント(フック)が含まれており、各プラグインはそのフックに接続して特定のアクションを実行できます。

その他の「イベント」

注:threading.Eventは、上記の意味での「イベントシステム」ではありません。これは、あるスレッドが別のスレッドがイベントオブジェクトに「シグナル」を送るまで待機するスレッド同期システムです。

ネットワークメッセージングライブラリでも「イベント」という用語がよく使用されます。これらの概念は似ている場合があります。時々そうではない。もちろん、スレッド、プロセス、およびコンピューターの境界を通過できます。たとえば、pyzmqpymqTwistedTornadogeventeventletを参照してください 。

弱い参照

Pythonでは、メソッドまたはオブジェクトへの参照を保持することで、ガベージコレクターによって削除されないようにします。これは望ましいことですが、メモリリークが発生する可能性もあります。リンクされたハンドラがクリーンアップされることはありません。

一部のイベントシステムでは、これを解決するために、通常の参照ではなく弱参照を使用しています。

さまざまなライブラリについて一言

オブザーバースタイルのイベントシステム:

  • zope.eventは、これがどのように機能するかについての本質を示しています(Lennartの回答を参照)。注:この例では、ハンドラー引数もサポートされていません。
  • LongPokeの「呼び出し可能リスト」の実装は、そのようなイベントシステムをサブクラス化することで非常に最小限に実装できることを示していlistます。
  • フェルクのバリエーションEventHookは、呼び出し先と呼び出し元の署名も保証します。
  • spassigのEventHook(Michael Foordのイベントパターン)は簡単な実装です。
  • JosipのValued Lessons Eventクラスは基本的に同じですが、バッグの保存にのset代わりにを使用し、list実装__call__はどちらも合理的な追加です。
  • PyNotifyは概念が似ており、変数と条件の追加の概念(「変数変更イベント」)も提供します。ホームページが機能していません。
  • axelは基本的に、スレッド化、エラー処理などに関連する機能を持つバッグオブハンドラーです。
  • python-dispatchはから派生する偶数のソースクラスを必要としますpydispatch.Dispatcher
  • buslaneはクラスベースで、単一または複数のハンドラーをサポートし、広範な型ヒントを容易にします。
  • ピティコスのオブザーバー/イベントは軽量設計です。

パブリッシュ/サブスクライブライブラリ:

  • ブリンカーには、自動切断や送信者に基づくフィルタリングなどの気の利いた機能がいくつかあります。
  • PyPubSubは安定したパッケージであり、「トピックとメッセージのデバッグと保守を容易にする高度な機能」を約束します。
  • pymitterはNode.js EventEmitter2のPythonポートであり、名前空間、ワイルドカード、TTLを提供します。
  • PyDispatcherは、多対多の公開などに関して柔軟性を強調するようです。弱い参照をサポートします。
  • ルイは PyDispatcherを再加工し、「様々な文脈で」動作するはずです。
  • pypydispatcherは(ご想像のとおり...)PyDispatcherに基づいており、PyPyでも動作します。
  • django.dispatchは、PyDispatcherを書き直したもので、「インターフェースは制限されていますが、パフォーマンスは向上しています」。
  • pyeventdispatcherは、PHPのSymfonyフレームワークのイベントディスパッチャーに基づいています。
  • ディスパッチャーはdjango.dispatchから抽出されましたが、かなり古くなっています。
  • Cristian GarciaのEventMangerは本当に短い実装です。

その他:

  • pluggyには、pytestプラグインによって使用されるフックシステムが含まれています。
  • RxPy3はObservableパターンを実装し、イベントのマージ、再試行などを可能にします。
  • QtのシグナルとスロットはPyQt またはPySide2から利用できます。それらは、同じスレッドで使用される場合はコールバックとして、または2つの異なるスレッド間のイベント(イベントループを使用)として機能します。シグナルとスロットには、から派生するクラスのオブジェクトでのみ機能するという制限がありますQObject

2
PyDispatcherに基づいたルイもあります:pypi.python.org/pypi/Louie/1.1
the979kid

@ the979kid louieはメンテナンスが不十分なようです。pypiページはGitHubの404にリンクしています:11craft.github.io/louie ; github.com/gldnspud/louiegithub.com/11craft/louieである必要があります。
florisla 2016年

1
弱められたイベントリスナーは一般的なニーズです。そうしないと、実際の使用法は困難になります。役立つと思われるソリューションをサポートするメモ。
kxr

Pypubsub 4は多対多で、メッセージの強力なデバッグツールと、メッセージペイロードを制限するいくつかの方法を備えているため、無効なデータを送信したり、データが欠落したりしたことがわかります。PyPubSub 4はPython 3をサポートしています(PyPubSub 3.xはPython 2をサポートしています)。
オリバー

最近、pymq github.com/thrau/pymqというライブラリを公開しました。
thrau

98

私はこのようにしてきました:

class Event(list):
    """Event subscription.

    A list of callable objects. Calling an instance of this will cause a
    call to each item in the list in ascending order by index.

    Example Usage:
    >>> def f(x):
    ...     print 'f(%s)' % x
    >>> def g(x):
    ...     print 'g(%s)' % x
    >>> e = Event()
    >>> e()
    >>> e.append(f)
    >>> e(123)
    f(123)
    >>> e.remove(f)
    >>> e()
    >>> e += (f, g)
    >>> e(10)
    f(10)
    g(10)
    >>> del e[0]
    >>> e(2)
    g(2)

    """
    def __call__(self, *args, **kwargs):
        for f in self:
            f(*args, **kwargs)

    def __repr__(self):
        return "Event(%s)" % list.__repr__(self)

しかし、私が見た他のすべてと同様に、これのために自動生成されたpydocも、署名もありません。


3
私はこのスタイルがかなり興味深いと思います。それは甘い裸の骨です。イベントとそのサブスクライバーを自律操作として操作できるという点が気に入っています。実際のプロジェクトでどうなるか見ていきます。
Rudy Lattae、2010

2
とても美しいミニマルスタイル!素晴らしい!
akaRem 2013

2
私はこれを十分に賛成することはできません、これは本当に簡単で簡単です。

2
私の10歳のように誰かがこれを説明できますか?このクラスはメインクラスに継承されますか?私は見ていないのinitので、スーパー()は使用できません。どういうわけか私のためにクリックしていません。
omgimdrunk 2017

1
@omgimdrunk単純なイベントハンドラーは、イベントが発生するたびに1つ以上の呼び出し可能な関数を起動します。これを「管理」するクラスでは、少なくとも次のメソッドが必要になります-追加と起動。そのクラス内では、実行するハンドラーのリストを維持する必要があります。_bag_of_handlersそれをリストであるインスタンス変数に入れましょう。クラスのaddメソッドは単にになりますself._bag_of_handlers.append(some_callable)。クラスのfireメソッドは `_bag_of_handlers`を介してループし、提供されたargsとkwargsをハンドラーに渡し、それぞれを順番に実行します。
Gabe Spradlin 2017

68

Michael Foordのイベントパターンで提案されているように、EventHookを使用します。

EventHooksをクラスに追加するだけです。

class MyBroadcaster()
    def __init__():
        self.onChange = EventHook()

theBroadcaster = MyBroadcaster()

# add a listener to the event
theBroadcaster.onChange += myFunction

# remove listener from the event
theBroadcaster.onChange -= myFunction

# fire event
theBroadcaster.onChange.fire()

オブジェクトからすべてのリスナーを削除する機能をMichaelsクラスに追加すると、次のようになります。

class EventHook(object):

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

    def __iadd__(self, handler):
        self.__handlers.append(handler)
        return self

    def __isub__(self, handler):
        self.__handlers.remove(handler)
        return self

    def fire(self, *args, **keywargs):
        for handler in self.__handlers:
            handler(*args, **keywargs)

    def clearObjectHandlers(self, inObject):
        for theHandler in self.__handlers:
            if theHandler.im_self == inObject:
                self -= theHandler

これを使用することの欠点は、サブスクライバーとして登録する前に、まずイベントを追加する必要があることです。パブリッシャーのみがイベントを追加する場合(必須ではなく、単なる良い習慣)、サブスクライバーの前にパブリッシャーを初期化する必要があります。これは、大規模なプロジェクトでは苦痛です
Jonathan

6
反復中にself .__ handlersが変更されるため、最後のメソッドにバグがあります。修正: `self .__ handlers = [h for h。in self .__ handlers if h.im_self!= obj]`
Simon Bergot

1
@Simonは正しいですが、self .__ handlersにバインドされていない関数を含めることができるため、バグが発生します。修正:self.__handlers = [h for h in self._handlers if getattr(h, 'im_self', False) != obj]
Eric Marcos

20

私はzope.eventを使用します。それはあなたが想像できる最も裸の骨です。:-)実際、ここに完全なソースコードがあります:

subscribers = []

def notify(event):
    for subscriber in subscribers:
        subscriber(event)

たとえば、プロセス間でメッセージを送信できないことに注意してください。それはメッセージングシステムではなく、単なるイベントシステムです。


17
pypi.python.org/pypi/zope.event ...貧弱なGoogleに帯域幅をいくらか保存するには;-)
Boldewyn

メッセージを送信できるようにしたいのですが。Tkinterで構築されたアプリケーションでイベントシステムを使用します。メッセージをサポートしていないため、イベントシステムを使用していません。
ジョシップ

zope.eventを使用して、好きなものを送信できます。しかし、私のポイントは、他のプロセスや他のコンピューターにイベントやメッセージを送信できないため、適切なメッセージングシステムではないということです。あなたはおそらくあなたの要件をもっと具体的にする必要があります。
Lennart Regebro 2009

15

価値あるレッスンでこの小さなスクリプトを見つけました。それは私が求めている適切なシンプルさ/パワー比を持っているようです。Peter Thatcherは次のコードの作成者です(ライセンスについては言及されていません)。

class Event:
    def __init__(self):
        self.handlers = set()

    def handle(self, handler):
        self.handlers.add(handler)
        return self

    def unhandle(self, handler):
        try:
            self.handlers.remove(handler)
        except:
            raise ValueError("Handler is not handling this event, so cannot unhandle it.")
        return self

    def fire(self, *args, **kargs):
        for handler in self.handlers:
            handler(*args, **kargs)

    def getHandlerCount(self):
        return len(self.handlers)

    __iadd__ = handle
    __isub__ = unhandle
    __call__ = fire
    __len__  = getHandlerCount

class MockFileWatcher:
    def __init__(self):
        self.fileChanged = Event()

    def watchFiles(self):
        source_path = "foo"
        self.fileChanged(source_path)

def log_file_change(source_path):
    print "%r changed." % (source_path,)

def log_file_change2(source_path):
    print "%r changed!" % (source_path,)

watcher              = MockFileWatcher()
watcher.fileChanged += log_file_change2
watcher.fileChanged += log_file_change
watcher.fileChanged -= log_file_change2
watcher.watchFiles()

1
リストの代わりにset()を使用すると、ハンドラーが2回登録されることを回避できます。1つの結果は、ハンドラーが登録された順序で呼び出されていないことです。必ずしも悪いことではありませんが...
florisla 2013

1
@florislaは、必要に応じてOrderedSetと交換できます。
ロビノ2018年

9

以下は、正常に機能する必要のある最小限のデザインです。あなたがしなければならないことは、単にObserverクラスを継承し、その後observe(event_name, callback_fn)特定のイベントをリッスンするために使用することです。特定のイベントがコード内のどこか(つまりEvent('USB connected'))で発生すると、対応するコールバックが発生します。

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observed_events = []
    def observe(self, event_name, callback_fn):
        self._observed_events.append({'event_name' : event_name, 'callback_fn' : callback_fn})


class Event():
    def __init__(self, event_name, *callback_args):
        for observer in Observer._observers:
            for observable in observer._observed_events:
                if observable['event_name'] == event_name:
                    observable['callback_fn'](*callback_args)

例:

class Room(Observer):
    def __init__(self):
        print("Room is ready.")
        Observer.__init__(self) # DON'T FORGET THIS
    def someone_arrived(self, who):
        print(who + " has arrived!")

# Observe for specific event
room = Room()
room.observe('someone arrived',  room.someone_arrived)

# Fire some events
Event('someone left',    'John')
Event('someone arrived', 'Lenard') # will output "Lenard has arrived!"
Event('someone Farted',  'Lenard')

私はあなたのデザインが好きです、それはミニマルで理解しやすいものです。一部のモジュールをインポートする必要がないため、軽量になります。
Atreyagaurav

8

EventManagerクラスを作成しました(最後のコード)。構文は次のとおりです。

#Create an event with no listeners assigned to it
EventManager.addEvent( eventName = [] )

#Create an event with listeners assigned to it
EventManager.addEvent( eventName = [fun1, fun2,...] )

#Create any number event with listeners assigned to them
EventManager.addEvent( eventName1 = [e1fun1, e1fun2,...], eventName2 = [e2fun1, e2fun2,...], ... )

#Add or remove listener to an existing event
EventManager.eventName += extra_fun
EventManager.eventName -= removed_fun

#Delete an event
del EventManager.eventName

#Fire the event
EventManager.eventName()

次に例を示します。

def hello(name):
    print "Hello {}".format(name)
    
def greetings(name):
    print "Greetings {}".format(name)

EventManager.addEvent( salute = [greetings] )
EventManager.salute += hello

print "\nInitial salute"
EventManager.salute('Oscar')

print "\nNow remove greetings"
EventManager.salute -= greetings
EventManager.salute('Oscar')

出力:

最初の敬礼
挨拶オスカー
ハローオスカー

今挨拶を削除します
Hello Oscar

EventMangerコード:

class EventManager:
    
    class Event:
        def __init__(self,functions):
            if type(functions) is not list:
                raise ValueError("functions parameter has to be a list")
            self.functions = functions
            
        def __iadd__(self,func):
            self.functions.append(func)
            return self
            
        def __isub__(self,func):
            self.functions.remove(func)
            return self
            
        def __call__(self,*args,**kvargs):
            for func in self.functions : func(*args,**kvargs)
            
    @classmethod
    def addEvent(cls,**kvargs):
        """
        addEvent( event1 = [f1,f2,...], event2 = [g1,g2,...], ... )
        creates events using **kvargs to create any number of events. Each event recieves a list of functions,
        where every function in the list recieves the same parameters.
        
        Example:
        
        def hello(): print "Hello ",
        def world(): print "World"
        
        EventManager.addEvent( salute = [hello] )
        EventManager.salute += world
        
        EventManager.salute()
        
        Output:
        Hello World
        """
        for key in kvargs.keys():
            if type(kvargs[key]) is not list:
                raise ValueError("value has to be a list")
            else:
                kvargs[key] = cls.Event(kvargs[key])
        
        cls.__dict__.update(kvargs)

8

あなたはpymitterpypi)を見ているかもしれません。その小さな単一ファイル(約250 loc)のアプローチは、「名前空間、ワイルドカード、TTLを提供する」ものです。

基本的な例は次のとおりです。

from pymitter import EventEmitter

ee = EventEmitter()

# decorator usage
@ee.on("myevent")
def handler1(arg):
   print "handler1 called with", arg

# callback usage
def handler2(arg):
    print "handler2 called with", arg
ee.on("myotherevent", handler2)

# emit
ee.emit("myevent", "foo")
# -> "handler1 called with foo"

ee.emit("myotherevent", "bar")
# -> "handler2 called with bar"

6

Longpokeの最小限のアプローチのバリエーションを作成し、呼び出し先と呼び出し元の両方の署名も保証します。

class EventHook(object):
    '''
    A simple implementation of the Observer-Pattern.
    The user can specify an event signature upon inizializazion,
    defined by kwargs in the form of argumentname=class (e.g. id=int).
    The arguments' types are not checked in this implementation though.
    Callables with a fitting signature can be added with += or removed with -=.
    All listeners can be notified by calling the EventHook class with fitting
    arguments.

    >>> event = EventHook(id=int, data=dict)
    >>> event += lambda id, data: print("%d %s" % (id, data))
    >>> event(id=5, data={"foo": "bar"})
    5 {'foo': 'bar'}

    >>> event = EventHook(id=int)
    >>> event += lambda wrong_name: None
    Traceback (most recent call last):
        ...
    ValueError: Listener must have these arguments: (id=int)

    >>> event = EventHook(id=int)
    >>> event += lambda id: None
    >>> event(wrong_name=0)
    Traceback (most recent call last):
        ...
    ValueError: This EventHook must be called with these arguments: (id=int)
    '''
    def __init__(self, **signature):
        self._signature = signature
        self._argnames = set(signature.keys())
        self._handlers = []

    def _kwargs_str(self):
        return ", ".join(k+"="+v.__name__ for k, v in self._signature.items())

    def __iadd__(self, handler):
        params = inspect.signature(handler).parameters
        valid = True
        argnames = set(n for n in params.keys())
        if argnames != self._argnames:
            valid = False
        for p in params.values():
            if p.kind == p.VAR_KEYWORD:
                valid = True
                break
            if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY):
                valid = False
                break
        if not valid:
            raise ValueError("Listener must have these arguments: (%s)"
                             % self._kwargs_str())
        self._handlers.append(handler)
        return self

    def __isub__(self, handler):
        self._handlers.remove(handler)
        return self

    def __call__(self, *args, **kwargs):
        if args or set(kwargs.keys()) != self._argnames:
            raise ValueError("This EventHook must be called with these " +
                             "keyword arguments: (%s)" % self._kwargs_str())
        for handler in self._handlers[:]:
            handler(**kwargs)

    def __repr__(self):
        return "EventHook(%s)" % self._kwargs_str()

3

pyQtでコードを実行する場合、QTソケット/シグナルパラダイムを使用します。これはdjangoでも同じです

非同期I / Oを実行している場合は、ネイティブ選択モジュールを使用します

SAX Pythonパーサーを使用している場合は、SAXが提供するイベントAPIを使用しています。だから私は基礎となるAPIの犠牲者のようです:-)

イベントフレームワーク/モジュールに何を期待しているのかを自問する必要があるかもしれません。私の個人的な好みは、QTのソケット/シグナルパラダイムを使用することです。それについてのより多くの情報はここで見つけることができます


2

検討のための別のモジュールを次に示します。これは、より要求の厳しいアプリケーションでは実行可能な選択肢のようです。

Py-notifyは、オブザーバープログラミングパターンを実装するためのツールを提供するPythonパッケージです。これらのツールには、信号、条件、変数が含まれます。

シグナルは、シグナルが発行されたときに呼び出されるハンドラーのリストです。条件は、基本的にはブール変数であり、条件の状態が変化したときに発生する信号と組み合わされます。これらは、標準の論理演算子(not、andなど)を使用して組み合わせて複合条件にすることができます。変数は、条件とは異なり、ブール値だけでなく任意のPythonオブジェクトを保持できますが、組み合わせることはできません。


1
ホームページはこのページでは使用できません。おそらくサポートされなくなっていますか?
デビッドパークス

1

イベントのマージや再試行などのより複雑なことを実行したい場合は、Observableパターンとそれを実装する成熟したライブラリを使用できます。https://github.com/ReactiveX/RxPY。オブザーバブルはJavascriptおよびJavaで非常に一般的であり、一部の非同期タスクで使用するのに非常に便利です。

from rx import Observable, Observer


def push_five_strings(observer):
        observer.on_next("Alpha")
        observer.on_next("Beta")
        observer.on_next("Gamma")
        observer.on_next("Delta")
        observer.on_next("Epsilon")
        observer.on_completed()


class PrintObserver(Observer):

    def on_next(self, value):
        print("Received {0}".format(value))

    def on_completed(self):
        print("Done!")

    def on_error(self, error):
        print("Error Occurred: {0}".format(error))

source = Observable.create(push_five_strings)

source.subscribe(PrintObserver())

出力

Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon
Done!

1

あなたがプロセスまたはネットワーク境界を越えて働くeventbusが必要な場合は、試すことができPyMQを。現在、pub / sub、メッセージキュー、同期RPCをサポートしています。デフォルトバージョンはRedisバックエンドの上で動作するため、実行中のRedisサーバーが必要です。テスト用のインメモリバックエンドもあります。独自のバックエンドを作成することもできます。

import pymq

# common code
class MyEvent:
    pass

# subscribe code
@pymq.subscriber
def on_event(event: MyEvent):
    print('event received')

# publisher code
pymq.publish(MyEvent())

# you can also customize channels
pymq.subscribe(on_event, channel='my_channel')
pymq.publish(MyEvent(), channel='my_channel')

システムを初期化するには:

from pymq.provider.redis import RedisConfig

# starts a new thread with a Redis event loop
pymq.init(RedisConfig())

# main application control loop

pymq.shutdown()

免責事項:私はこのライブラリの作者です


0

buslaneモジュールを試すことができます。

このライブラリは、メッセージベースのシステムの実装を容易にします。コマンド(単一ハンドラー)およびイベント(0または複数のハンドラー)アプローチをサポートします。Buslaneは、ハンドラーを適切に登録するためにPython型注釈を使用します。

簡単な例:

from dataclasses import dataclass

from buslane.commands import Command, CommandHandler, CommandBus


@dataclass(frozen=True)
class RegisterUserCommand(Command):
    email: str
    password: str


class RegisterUserCommandHandler(CommandHandler[RegisterUserCommand]):

    def handle(self, command: RegisterUserCommand) -> None:
        assert command == RegisterUserCommand(
            email='john@lennon.com',
            password='secret',
        )


command_bus = CommandBus()
command_bus.register(handler=RegisterUserCommandHandler())
command_bus.execute(command=RegisterUserCommand(
    email='john@lennon.com',
    password='secret',
))

buslaneをインストールするには、単にpipを使用します。

$ pip install buslane

0

少し前に私はあなたに役立つかもしれないライブラリを書きました。ローカルリスナーとグローバルリスナー、それらを登録する複数の異なる方法、実行優先度などを設定できます。

from pyeventdispatcher import register

register("foo.bar", lambda event: print("second"))
register("foo.bar", lambda event: print("first "), -100)

dispatch(Event("foo.bar", {"id": 1}))
# first second

pyeventdispatcherを見てください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.