Python duck-typowanie do obsługi zdarzeń MVC w pygame

Razem z przyjacielem bawiliśmy się trochę pygame i natknęliśmy się na ten tutorial do budowania gier za pomocą pygame. Bardzo nam się podobało, jak przerobiono grę na system model-widok-kontroler z zdarzeniami jako pośrednikiem, ale kod sprawia, że ciężkie użycie isinstance sprawdza system zdarzeń.

Przykład:

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

To daje jakiś niezwykle niepytoniczny kod. Czy ktoś ma jakieś sugestie, jak można to poprawić? Lub alternatywą metodologia implementacji MVC?


To jest kawałek kodu, który napisałem na podstawie odpowiedzi @Mark-Hildreth (jak linkować użytkowników? Czy ktoś jeszcze ma jakieś dobre propozycje? Zostawię to otwarte na inny dzień, zanim wybierze rozwiązanie.

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()

To kolejny build z wykorzystaniem przykładów z @ Paul - imponująco proste!

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()
Author: Petriborg, 2011-08-31

3 answers

Czystszym sposobem obsługi zdarzeń (a także dużo szybszym, ale prawdopodobnie zużywającym nieco więcej pamięci) jest posiadanie wielu funkcji obsługi zdarzeń w kodzie. Coś w tym stylu:

Pożądany Interfejs

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

Tutaj, metoda __init__ otrzymuje EventDispatcher jako argument. Funkcja EventDispatcher.add przyjmuje teraz Rodzaj wydarzenia, które Cię interesuje, oraz słuchacza.

[33]}ma to korzystny wpływ na efektywność, ponieważ słuchacz jest wzywany tylko na wydarzenia że jest zainteresowany. W 1997 roku w wyniku połączenia się z grupą, w 1998 roku, doszło do połączenia grupy z grupą.]}

EventDispatcher implementacja

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

Ale jest problem z tą implementacją. Wewnątrz NotifyThisClass robisz to:

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

Problem polega na self.on_keyboard_event: jest to metoda związana , którą przekazałeś EventDispatcher. Metody związane zawierają odniesienie do self; oznacza to, że dopóki EventDispatcher ma metodę związaną, self nie będzie usunięte.

WeakBoundMethod

Będziesz musiał utworzyć WeakBoundMethod klasę, która zawiera tylko słabe odniesienia do self (widzę, że już wiesz o słabych odniesieniach), tak aby EventDispatcher nie zapobiegała usunięciu self.

Alternatywą byłoby posiadanie NotifyThisClass.remove_listeners funkcji, którą wywołujesz przed usunięciem obiektu, ale to nie jest najczystsze rozwiązanie i uważam, że jest bardzo podatne na błędy (łatwe do zapomnienia).

Implementacja WeakBoundMethod wyglądałaby coś takiego:

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)

Oto bardziej rozbudowana implementacja , którą zamieściłem na CodeReview, a oto przykład użycia klasy:

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 Obiekty (Opcjonalne)

Podczas usuwania słuchaczy z menedżera / dyspozytora, zamiast niepotrzebnie przeszukiwać słuchaczy, aż znajdzie właściwy typ zdarzenia, a następnie przeszukiwać listę, aż znajdzie WŁAŚCIWEGO słuchacza, możesz mieć coś takiego jak to:

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))
        ]

Tutaj EventDispatcher.add zwraca Connection obiekt, który wie, gdzie w dict EventDispatcher List się znajduje. Gdy NotifyThisClass obiekt jest usuwany, tak jest self._connections, który wywoła Connection.__del__, który usunie słuchacz z EventDispatcher.

To może sprawić, że Twój kod będzie szybszy i łatwiejszy w użyciu, ponieważ musisz tylko wyraźnie dodać funkcje, są one usuwane automatycznie, ale to do ciebie należy decyzja, czy chcesz to zrobić. Jeśli to zrobisz, zauważ, że EventDispatcher.remove nie powinno istnieć już nie.

 12
Author: Paul Manta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-13 12:40:32

Natknąłem się na samouczek SJ Browna na temat tworzenia gier w przeszłości. Świetna strona, jedna z najlepszych jakie czytałem. Jednak, podobnie jak ty, nie podobały mi się telefony do isinstance, ani fakt, że wszyscy słuchacze odbierają wszystkie wydarzenia.

Po pierwsze, isinstance jest wolniejsze niż sprawdzanie, czy dwa ciągi znaków są równe, więc skończyło się na zapisaniu nazwy w moich zdarzeniach i przetestowaniu nazwy, a nie Klasy. Ale i tak funkcja powiadomień z baterią if swędzi mnie, ponieważ czułem się jak strata czasu. Tutaj możemy wykonać dwie optymalizacje:

  1. większość słuchaczy interesuje się tylko kilkoma rodzajami wydarzeń. Ze względów wydajnościowych, gdy quitevent jest publikowany, tylko zainteresowani nim słuchacze powinni być powiadamiani. Menedżer wydarzeń śledzi, który słuchacz chce słuchać tego wydarzenia.
  2. Następnie, aby uniknąć przechodzenia przez tony IF wypowiedzi w jednej metodzie notify, będziemy mieli jedną metodę na typ wydarzenie.

Przykład:

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

Ponieważ chcę, aby programista pisał jak najmniej, zrobiłem następujące rzeczy:

Gdy słuchacz jest zarejestrowany w Menedżerze zdarzeń, menedżer zdarzeń skanuje wszystkie metody słuchacza. Gdy jedna z metod zaczyna się od 'on' (lub dowolnego prefiksu, który Ci się podoba), to patrzy na resztę ("QuitEvent") i wiąże tę nazwę z tą metodą. Później, gdy menedżer zdarzeń pompuje swoją listę zdarzeń, patrzy na nazwę klasy zdarzeń: "QuitEvent". Informatyka zna tę nazwę i dlatego może bezpośrednio wywoływać wszystkie odpowiednie procedury obsługi zdarzeń. Programista nie ma nic do roboty, ale dodanie metod whateverevent, aby działały.

Ma pewne wady:

  1. jeśli zrobię literówkę w nazwie Handlera ("onRunPhysicsEvent" zamiast "onPhysicsRanEvent" na przykład" ) wtedy mój handler będzie nigdy nie daj się wzywać, a zastanowię się dlaczego. Ale znam sztuczkę, więc ... nie zastanawiaj się, dlaczego tak długo.
  2. nie mogę dodać zdarzenia opiekun po tym, jak słuchacz został zarejestrowany. Muszę się wyrejestrować i ponownie zarejestrować. Rzeczywiście, obsługa zdarzeń jest skanowana tylko podczas rejestracji. Wtedy i tak nigdy nie musiałem tego robić, żeby tego nie przegapić.

Pomimo tych wad podoba mi się to o wiele bardziej niż to, że konstruktor słuchacza wyraźnie wyjaśnia menedżerowi Wydarzenia, że chce być na bieżąco z tym, tym, tym i tym wydarzeniem. I to i tak ta sama prędkość wykonania.

Drugi punkt:

Projektując naszego event managera, chcemy być ostrożni. Bardzo często słuchacz reaguje na zdarzenie, tworząc-rejestrując lub nie rejestrując-niszcząc słuchaczy. To się zdarza cały czas. Jeśli o tym nie pomyślimy, nasza gra może zerwać z RuntimeError: dictionary changed size podczas iteracji . Kod, który proponujesz iteruje nad kopią słownika, dzięki czemu jesteś chroniony przed eksplozjami; ale ma konsekwencje, aby być świadomym: - Słuchacze zarejestrowany z powodu wydarzenia nie otrzyma tego wydarzenia. - Słuchacze niezarejestrowani z powodu zdarzenia nadal otrzymają, że wydarzenie. Nigdy nie uważałem tego za problem.

Zaimplementowałem to do gry, którą rozwijam. Mogę linkować do dwóch i pół artykułów, które napisałem na temat:

Linki do mojego konta github doprowadzą Cię bezpośrednio do kodu źródłowego odpowiednich części. Jeśli nie możesz czekać, oto sprawa: https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py . Tam zobaczysz, że kod dla mojej klasy event jest trochę duży, ale każde odziedziczone zdarzenie jest zadeklarowane w 2 liniach: podstawowa klasa Event ułatwia Ci życie.

To wszystko działa przy użyciu mechanizmu introspekcji Pythona i przy użyciu faktu, że metody są obiektami, jak każdy inny, które można umieścić w słownikach. Myślę, że to dość pythony :).

 2
Author: Niriel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-03 14:55:29

Nadaje każdemu zdarzeniu metodę (być może nawet używając __call__) i przekazuje w obiekcie kontrolera jako argument. Metoda "call"powinna następnie wywołać obiekt controller. Na przykład...

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):
        ...

Jakikolwiek kod używasz do kierowania zdarzeń do kontrolerów wywoła metodę __call__ z odpowiednim kontrolerem.

 1
Author: Mark Hildreth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-30 20:37:13