9

友人と私はpygameで遊んでいて、pygameを使用してゲームを構築するためのこのチュートリアルに出くわしました。ゲームをモデルビューコントローラーシステムに分割し、イベントを仲介する方法が本当に気に入りましたが、コードはイベントシステムのチェックを多用しています。isinstance

例:

class CPUSpinnerController:
    ...
    def Notify(self, event):
        if isinstance( event, QuitEvent ):
            self.keepGoing = 0

これにより、非常に非Python的なコードが生成されます。これをどのように改善できるかについて誰かが何か提案がありますか?または、MVCを実装するための代替方法論ですか?


これは、@ Mark-Hildrethの回答に基づいて作成したコードです(ユーザーをリンクするにはどうすればよいですか?)他に良い提案はありますか?解決策を選ぶ前に、これをもう1日ほど開いたままにしておきます。

class EventManager:
    def __init__(self):
        from weakref import WeakKeyDictionary
        self.listeners = WeakKeyDictionary()

    def add(self, listener):
        self.listeners[ listener ] = 1

    def remove(self, listener):
        del self.listeners[ listener ]

    def post(self, event):
        print "post event %s" % event.name
        for listener in self.listeners.keys():
            listener.notify(event)

class Listener:
    def __init__(self, event_mgr=None):
        if event_mgr is not None:
            event_mgr.add(self)

    def notify(self, event):
        event(self)


class Event:
    def __init__(self, name="Generic Event"):
        self.name = name

    def __call__(self, controller):
        pass

class QuitEvent(Event):
    def __init__(self):
        Event.__init__(self, "Quit")

    def __call__(self, listener):
        listener.exit(self)

class RunController(Listener):
    def __init__(self, event_mgr):
        Listener.__init__(self, event_mgr)
        self.running = True
        self.event_mgr = event_mgr

    def exit(self, event):
        print "exit called"
        self.running = False

    def run(self):
        print "run called"
        while self.running:
            event = QuitEvent()
            self.event_mgr.post(event)

em = EventManager()
run = RunController(em)
run.run()

これは@Paulの例を使用した別のビルドです-非常にシンプルです!

class WeakBoundMethod:
    def __init__(self, meth):
        import weakref
        self._self = weakref.ref(meth.__self__)
        self._func = meth.__func__

    def __call__(self, *args, **kwargs):
        self._func(self._self(), *args, **kwargs)

class EventManager:
    def __init__(self):
        # does this actually do anything?
        self._listeners = { None : [ None ] }

    def add(self, eventClass, listener):
        print "add %s" % eventClass.__name__
        key = eventClass.__name__

        if (hasattr(listener, '__self__') and
            hasattr(listener, '__func__')):
            listener = WeakBoundMethod(listener)

        try:
            self._listeners[key].append(listener)
        except KeyError:
            # why did you not need this in your code?
            self._listeners[key] = [listener]

        print "add count %s" % len(self._listeners[key])

    def remove(self, eventClass, listener):
        key = eventClass.__name__
        self._listeners[key].remove(listener)

    def post(self, event):
        eventClass = event.__class__
        key = eventClass.__name__
        print "post event %s (keys %s)" % (
            key, len(self._listeners[key]))
        for listener in self._listeners[key]:
            listener(event)

class Event:
    pass

class QuitEvent(Event):
    pass

class RunController:
    def __init__(self, event_mgr):
        event_mgr.add(QuitEvent, self.exit)
        self.running = True
        self.event_mgr = event_mgr

    def exit(self, event):
        print "exit called"
        self.running = False

    def run(self):
        print "run called"
        while self.running:
            event = QuitEvent()
            self.event_mgr.post(event)

em = EventManager()
run = RunController(em)
run.run()
4

4 に答える 4

14

イベントをよりクリーンに処理する方法(さらに高速ですが、メモリを少し消費する可能性があります)は、コードに複数のイベントハンドラー関数を含めることです。これらの線に沿った何か:

必要なインターフェイス

class KeyboardEvent:
    pass

class MouseEvent:
    pass

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self.ed.add(KeyboardEvent, self.on_keyboard_event)
        self.ed.add(MouseEvent, self.on_mouse_event)

    def __del__(self):
        self.ed.remove(KeyboardEvent, self.on_keyboard_event)
        self.ed.remove(MouseEvent, self.on_mouse_event)

    def on_keyboard_event(self, event):
        pass

    def on_mouse_event(self, event):
        pass

ここで、__init__メソッドはEventDispatcher引数としてを受け取ります。このEventDispatcher.add関数は、関心のあるイベントのタイプとリスナーを取得します。

これは、リスナーが関心のあるイベントに対してのみ呼び出されるため、効率にメリットがあります。また、リスナーEventDispatcher自体の内部により一般的なコードが生成されます。

EventDispatcher実装

class EventDispatcher:
    def __init__(self):
        # Dict that maps event types to lists of listeners
        self._listeners = dict()

    def add(self, eventcls, listener):
        self._listeners.setdefault(eventcls, list()).append(listener)

    def post(self, event):
        try:
            for listener in self._listeners[event.__class__]:
                listener(event)
        except KeyError:
            pass # No listener interested in this event

しかし、この実装には問題があります。NotifyThisClassあなたの中でこれをします:

self.ed.add(KeyboardEvent, self.on_keyboard_event)

問題は次のself.on_keyboard_eventとおりです。これは、に渡したバインドされたメソッドEventDispatcherです。バインドされたメソッドは、self;への参照を保持します。EventDispatcherこれは、にバインドされたメソッドがある限り、self削除されないことを意味します。

WeakBoundMethod

の削除を妨げないように、(弱参照についてはすでに知っていると思いますが)WeakBoundMethodへの弱参照のみを保持するクラスを作成する必要があります。selfEventDispatcherself

別の方法としてNotifyThisClass.remove_listeners、オブジェクトを削除する前に呼び出す関数を用意することもできますが、これは実際には最もクリーンなソリューションではなく、エラーが発生しやすいと思います(忘れがちです)。

の実装は次のWeakBoundMethodようになります。

class WeakBoundMethod:
    def __init__(self, meth):
        self._self = weakref.ref(meth.__self__)
        self._func = meth.__func__

    def __call__(self, *args, **kwargs):
        self._func(self._self(), *args, **kwargs)

これは私がCodeReviewに投稿したより堅牢な実装であり、クラスの使用方法の例は次のとおりです。

from weak_bound_method import WeakBoundMethod as Wbm

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event))
        self.ed.add(MouseEvent, Wbm(self.on_mouse_event))

Connectionオブジェクト(オプション)

マネージャー/ディスパッチャーからリスナーを削除する場合EventDispatcher、適切なイベントタイプが見つかるまでリスナーを不必要に検索し、次に適切なリスナーが見つかるまでリストを検索する代わりに、次のようになります。

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self._connections = [
            self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event)),
            self.ed.add(MouseEvent, Wbm(self.on_mouse_event))
        ]

ここでは、リストのdictのどこにあるかを知っているオブジェクトをEventDispatcher.add返します。オブジェクトが削除されると、も呼び出され、リスナーが。から削除されます。ConnectionEventDispatcherNotifyThisClassself._connectionsConnection.__del__EventDispatcher

これにより、関数を明示的に追加するだけでコードがより速く簡単に使用できるようになります。関数は自動的に削除されますが、これを行うかどうかはユーザー次第です。あなたがそれをするならば、それはEventDispatcher.removeもう存在するべきではないことに注意してください。

于 2011-09-03T15:56:13.123 に答える
2

私は過去にゲームを作ることに関するSJブラウンのチュートリアルに出くわしました。これは素晴らしいページで、私が読んだ中で最高のページの1つです。しかし、あなたのように、私はisinstanceの呼び出し、またはすべてのリスナーがすべてのイベントを受信するという事実が好きではありませんでした。

まず、isinstanceは、2つの文字列が等しいことを確認するよりも遅いため、イベントに名前を格納し、クラスではなく名前をテストすることになりました。しかし、それでも、時間の無駄のように感じたので、バッテリーを使った通知機能かゆみを感じていました。ここで2つの最適化を行うことができます。

  1. ほとんどのリスナーは、いくつかのタイプのイベントにのみ関心があります。パフォーマンス上の理由から、QuitEventが投稿されると、それに関心のあるリスナーのみに通知する必要があります。イベントマネージャは、どのリスナーがどのイベントを聞きたいかを追跡します。
  2. 次に、単一の通知メソッドで大量のifステートメントを実行しないようにするために、イベントのタイプごとに1つのメソッドがあります。

例:

class GameLoopController(...):
    ...
    def onQuitEvent(self, event):
        # Directly called by the event manager when a QuitEvent is posted.
        # I call this an event handler.
        self._running = False

開発者の入力をできるだけ少なくしたいので、次のようにしました。

リスナーがイベントマネージャーに登録されると、イベントマネージャーはリスナーのすべてのメソッドをスキャンします。1つのメソッドが「on」(または任意のプレフィックス)で始まる場合、残りのメソッド( "QuitEvent")を調べて、この名前をこのメソッドにバインドします。後で、イベントマネージャがイベントリストをポンプするときに、イベントクラス名「QuitEvent」を調べます。その名前を知っているため、対応するすべてのイベントハンドラーを直接呼び出すことができます。開発者は、onWhateverEventメソッドを追加してそれらを機能させる以外に何もする必要はありません。

これにはいくつかの欠点があります。

  1. ハンドラーの名前にタイプミスをすると(たとえば、「onPhysicsRanEvent」ではなく「onRunPhysicsEvent」)、ハンドラーが呼び出されることはなく、理由がわかります。しかし、トリックを知っているので、理由はわかりません。非常に長い。
  2. リスナーを登録した後、イベントハンドラーを追加できません。登録を解除して再登録する必要があります。実際、イベントハンドラーは、登録中にのみスキャンされます。それからまた、私はとにかくそれをする必要がなかったので、それを見逃すことはありません。

これらの欠点にもかかわらず、リスナーのコンストラクターがイベントマネージャーに、これ、これ、これ、およびこのイベントに注目し続けたいことを明示的に説明するよりも、私はそれが好きです。そして、それはとにかく同じ実行速度です。

2点目:

イベントマネージャーを設計するときは、注意が必要です。多くの場合、リスナーは、リスナーを作成(登録または登録解除)して破棄することにより、イベントに応答します。これは常に起こります。それについて考えないと、RuntimeErrorでゲームが中断する可能性があります。反復中に辞書のサイズが変更されました。提案するコードは辞書のコピーを反復処理するため、爆発から保護されます。ただし、次の点に注意する必要があります。-イベントのために登録されたリスナーは、そのイベントを受信しません。-イベントが原因で登録解除されたリスナーは、引き続きそのイベントを受信します。しかし、私はそれが問題になるとは思っていませんでした。

私は自分が開発しているゲームのためにそれを自分で実装しました。私がこの主題について書いた2つの記事と半分にあなたをリンクすることができます:

私のgithubアカウントへのリンクから、関連するパーツのソースコードに直接アクセスできます。待つことができない場合は、https ://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.pyをご覧ください。そこには、私のイベントクラスのコードが少し大きいことがわかりますが、継承されたすべてのイベントは2行で宣言されています。基本のイベントクラスはあなたの生活を楽にしてくれます。

したがって、これはすべて、Pythonのイントロスペクションメカニズムを使用し、メソッドが辞書に入れることができる他のオブジェクトと同様のオブジェクトであるという事実を使用して機能します。私はそれがかなりpythonyだと思います:)。

于 2011-09-03T14:55:29.783 に答える
1

各イベントにメソッドを与え(おそらくを使用する場合でも__call__)、引数としてControllerオブジェクトを渡します。次に、「call」メソッドはコントローラーオブジェクトを呼び出す必要があります。例えば...

class QuitEvent:
    ...
    def __call__(self, controller):
        controller.on_quit(self) # or possibly... controller.on_quit(self.val1, self.val2)

class CPUSpinnerController:
    ...
    def on_quit(self, event):
        ...

イベントをコントローラーにルーティングするために使用しているコードが何であれ__call__、正しいコントローラーでメソッドを呼び出します。

于 2011-08-30T20:37:13.603 に答える
0

私は同じ問題に遭遇しました(ほぼ10年後!)。これは、EventManagerがリスナーのサブセットのみに通知するように使用している実装です。
これはに基づいていますdefaultdict_listenersEventManagerの属性はのdefaultdictですWeakKeyDictionary()
イベントはすべて空の抽象Eventクラスから継承されるため、リスナーは聞きたいイベントの一部のクラスにのみ集中できます。
その背後にあるアイデアを得るためのミニマリストコードは次のとおりです。

from collections import defaultdict
from weakref import WeakKeyDictionary

class Event:
    def __init__(self):
       pass

class KeyboardEvent(Event): # for instance, a keyboard event class with the key pressed
    def __init__(self, key):
        self._key = key

class EventManager:
    def __init__(self):
        self._listeners = defaultdict(lambda: WeakKeyDictionary())

    def register_listener(self, event_types, listener):
        for event_type in event_types:
            self._listeners[event_type][listener] = 1

    def unregister_listener(self, listener):
        for event_type in self._listeners:
            self._listeners[event_type].pop(listener, None)

    def post_event(self, event):
        for listener in self._listeners[event.__class__]:
            listener.notify(event)

登録時に、リスナーはイベントマネージャーに通知するイベントタイプを通知します。
イベントを投稿する場合、イベントマネージャーは、そのタイプのイベントの通知を登録したリスナーにのみ通知します。
もちろん、このコードの範囲は、@ Paul Mantaによって提案された非常に一般的な(そして非常にエレガントな)ソリューションよりもはるかに狭いですが、私の場合は、私isinstanceと同じくらい単純に保ちながら、繰り返しの呼び出しやその他のチェックを削除するのに役立ちましたたぶん......だろう。
これの欠点の1つは、すべてのタイプのイベントが何らかのクラスのオブジェクトでなければならないことですが、オブジェクト指向Pythonでは、これが進むべき道であると考えられています。

于 2019-12-13T15:47:49.317 に答える