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()
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.
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.
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:
- 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.
- 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:
- 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.
- 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:
- http://niriel.wordpress.com/2011/08/06/who-controls-the-controllers/
- http://niriel.wordpress.com/2011/08/08/the-event-management-is-in-place/
- http://niriel.wordpress.com/2011/08/11/the-first-screenshot-of-infiniworld/
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 :).
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.
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