Python 鸭子类型用于 pygame 中的 MVC 事件处理
我和一个朋友一直在玩 pygame,并遇到了这个构建游戏的教程使用 pygame.我们真的很喜欢它如何将游戏分解为模型-视图-控制器系统,并以事件为中介,但代码大量使用了 isinstance
检查事件系统。
示例:
class CPUSpinnerController:
...
def Notify(self, event):
if isinstance( event, QuitEvent ):
self.keepGoing = 0
这会产生一些极其不Python的代码。有人对如何改进有任何建议吗?或者实现 MVC 的替代方法?
这是我根据 @Mark-Hildreth 答案编写的一些代码(如何链接用户?)还有其他人有什么好的建议吗?在选择解决方案之前,我将把这个问题再保留一天左右。
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()
A friend and I have been playing around with pygame some and came across this tutorial for building games using pygame. We really liked how it broke out the game into a model-view-controller system with events as a go-between, but the code makes heavy use of isinstance
checks for the event system.
Example:
class CPUSpinnerController:
...
def Notify(self, event):
if isinstance( event, QuitEvent ):
self.keepGoing = 0
This results in some extremely unpythonic code. Does anyone have any suggestions on how this could be improved? Or an alternative methodology for implementing MVC?
This is a bit of code I wrote based on @Mark-Hildreth answer (how do I link users?) Does anyone else have any good suggestions? I'm going to leave this open for another day or so before picking a solution.
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()
This is another build using the examples from @Paul - impressively simple!
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()
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
处理事件的一种更简洁的方法(而且速度更快,但可能消耗更多内存)是在代码中使用多个事件处理函数。沿着这些思路:
所需的接口
这里,
__init__
方法接收一个EventDispatcher
作为参数。EventDispatcher.add
函数现在采用您感兴趣的事件类型和侦听器。这对效率有好处,因为侦听器只会被其感兴趣的事件调用。它还会在
EventDispatcher
本身内部产生更通用的代码:EventDispatcher
实现是这个实现的一个问题。在
NotifyThisClass
中,您执行以下操作:问题在于
self.on_keyboard_event
:它是一个绑定方法,您将其传递给EventDispatcher< /代码>。绑定方法保存对 self 的引用;这意味着只要
EventDispatcher
有绑定方法,self
就不会被删除。WeakBoundMethod
您需要创建一个
WeakBoundMethod
类,该类仅保存对self
的弱引用(我发现您已经了解弱引用),以便EventDispatcher
code> 不会阻止删除self
。另一种方法是在删除对象之前调用一个
NotifyThisClass.remove_listeners
函数,但这并不是最干净的解决方案,而且我发现它很容易出错(很容易忘记这样做)。WeakBoundMethod
的实现看起来像这样:这是我发布的一个更强大的实现在 CodeReview 上,这里是如何使用该类的示例:
Connection
对象(可选)从管理器/调度程序中删除侦听器时,而不是使
EventDispatcher
无需搜索侦听器,直到找到正确的事件类型,然后搜索列表,直到找到正确的侦听器,您可能会得到如下内容:这里
EventDispatcher.add
返回一个Connection
对象,该对象知道它位于EventDispatcher
列表字典中的位置。当NotifyThisClass
对象被删除时,self._connections
也会被删除,它将调用Connection.__del__
,这将从>事件调度程序
。这可以使您的代码更快、更易于使用,因为您只需显式添加函数,它们会自动删除,但是否要执行此操作由您决定。如果您这样做,请注意
EventDispatcher.remove
不应再存在。A cleaner way of handling events (and also a lot faster, but possibly consumes a bit more memory) is to have multiple event handler functions in your code. Something along these lines:
The Desired Interface
Here, the
__init__
method receives anEventDispatcher
as an argument. TheEventDispatcher.add
function now takes the type of the event you are interested in, and the listener.This has benefits for efficiency since the listener only ever gets called for events that it is interested in. It also results in more generic code inside the
EventDispatcher
itself:EventDispatcher
ImplementationBut there is a problem with this implementation. Inside
NotifyThisClass
you do this:The problem is with
self.on_keyboard_event
: it is a bound method which you passed to theEventDispatcher
. Bound methods hold a reference toself
; this means that as long as theEventDispatcher
has the bound method,self
will not be deleted.WeakBoundMethod
You will need to create a
WeakBoundMethod
class that holds only a weak reference toself
(I see you already know about weak references) so that theEventDispatcher
does not prevent the deletion ofself
.An alternative would be to have a
NotifyThisClass.remove_listeners
function that you call before deleting the object, but that's not really the cleanest solution and I find it very error prone (easy to forget to do).The implementation of
WeakBoundMethod
would look something like this:Here's a more robust implementation I posted on CodeReview, and here's an example of how you'd use the class:
Connection
Objects (Optional)When removing listeners from the manager/ dispatcher, instead of making the
EventDispatcher
needlessly search through the listeners until it finds the right event type, then search through the list until it finds the right listener, you could have something like this:Here
EventDispatcher.add
returns aConnection
object that knows where in theEventDispatcher
's dict of lists it resides. When aNotifyThisClass
object is deleted, so isself._connections
, which will callConnection.__del__
, which will remove the listener from theEventDispatcher
.This could make your code both faster and easier to use because you only have to explicitly add the functions, they are removed automatically, but it's up to you to decide if you want to do this. If you do it, note that
EventDispatcher.remove
shouldn't exist anymore.我过去偶然发现了 SJ Brown 的游戏制作教程。这是一个很棒的页面,是我读过的最好的页面之一。但是,和您一样,我不喜欢对 isinstance 的调用,也不喜欢所有侦听器接收所有事件的事实。
首先, isinstance 比检查两个字符串是否相等要慢,因此我最终在事件中存储了一个名称并测试该名称而不是类。但是,带有 if 电池的通知函数仍然让我感到痒痒,因为它感觉像是浪费时间。我们可以在这里做两个优化:
示例:
因为我希望开发人员尽可能少地键入,所以我做了以下事情:
当侦听器注册到事件管理器时,事件管理器会扫描侦听器的所有方法。当一个方法以“on”(或您喜欢的任何前缀)开头时,它会查看其余部分(“QuitEvent”)并将该名称绑定到该方法。稍后,当事件管理器抽取其事件列表时,它会查看事件类名称:“QuitEvent”。它知道该名称,因此可以直接调用所有相应的事件处理程序。开发人员只需添加 onWhateverEvent 方法即可使其正常工作。
它有一些缺点:
而不是“onPhysicsRanEvent”,例如“)然后我的处理程序将
永远不会被叫到,我想知道为什么。但我知道诀窍所以我
不要奇怪为什么很长。
挂号的。我必须取消注册并重新注册。确实,
仅在注册期间扫描事件处理程序。然后
再说一次,无论如何我从来没有必要这样做,所以我不会错过它。
尽管有这些缺点,我更喜欢它,而不是让侦听器的构造函数明确地向事件管理器解释它想要密切关注这个、这个、这个和这个事件。无论如何,它的执行速度是相同的。
第二点:
在设计我们的事件管理器时,我们要小心。通常,侦听器将通过创建-注册或取消注册-销毁侦听器来响应事件。这种情况经常发生。如果我们不考虑这一点,那么我们的游戏可能会因RuntimeError:字典在迭代期间更改大小而中断。您建议的代码会迭代字典的副本,以便防止爆炸;但它的后果需要注意:
- 由于事件而注册的侦听器将不会收到该事件。
- 由于事件而未注册的听众仍会收到该消息
事件。
但我从来没有发现这是一个问题。
我自己为我正在开发的游戏实现了这一点。我可以将您链接到我就该主题撰写的两篇半文章:
我的github帐户的链接将直接带您到相关部分的源代码。如果您等不及了,请看以下内容:https://github。 com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py 。在那里,您会看到我的事件类的代码有点大,但每个继承的事件都是在两行中声明的:基事件类使您的生活变得轻松。
因此,这一切都可以使用 python 的自省机制来实现,并且使用方法是像任何其他可以放入字典中的对象一样的对象这一事实。我认为这很Pythony:)。
I stumbled upon SJ Brown's tutorial on making games in the past. It's a great page, one of the best I've read. However, like you, I didn't like the calls to isinstance, or the fact that all the listeners receive all the events.
First, isinstance is slower than checking that two strings are equals, so I ended up storing a name on my events and test for the name rather than the class. But still, the notify function with its battery of if was itching me because it felt like a waste of time. We can do two optimizations here:
Example:
Because I want the developer to type as little as possible, I made the following thing:
When a listener is registered to an event manager, the event manager scans all the methods of the listener. When one method starts with 'on' (or any prefix you like), then it looks at the rest ("QuitEvent") and binds this name to this method. Later, when the event manager pumps its event list, it looks at the event class name: "QuitEvent". It knows that name, and therefore can directly call all the corresponding event handlers directly. The developer has nothing to do but adding onWhateverEvent methods to have them working.
It has some drawbacks:
instead of "onPhysicsRanEvent" for example") then my handler will
never be called and I'll wonder why. But I know the trick so I
don't wonder why very long.
registered. I must un-register and re-register. Indeed, the
events handlers are scanned only during the registration. Then
again, I never had to do that anyway so I don't miss it.
Despite these drawbacks I like it much more than having the constructor of the listener explicitly explain the event manager that it wants to stay tuned of this, this, this and this event. And it's the same execution speed anyway.
Second point:
When designing our event manager, we want to be careful. Very often, a listener will respond to an event by creating-registering or unregistering-destroying listeners. This happens all the time. If we don't think about it then our game may break with RuntimeError: dictionary changed size during iteration. The code that you propose iterates over a copy of the dictionary so you're protected against explosions; but it has consequences to be aware of:
- Listeners registered because of an event will not receive that event.
- Listeners unregistered because of an event will still receive that
event.
I never found it to be a problem though.
I implemented that myself for the game I am developing. I can link you to two articles and a half I wrote on the subject:
The links to my github account will bring you directly to the source code of the relevant parts. If you cannot wait, here's the thing: https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py . In there you'll see that the code for my event class is a bit big, but that every inherited event is declared in 2 lines: the base Event class is making your life easy.
So, this all works using python's introspection mechanism, and using the fact that methods are objects like any other that can be put in dictionaries. I think it's quite pythony :).
为每个事件提供一个方法(甚至可能使用 __call__ ),并传入 Controller 对象作为参数。然后,“call”方法应该调用控制器对象。例如...
无论您使用什么代码将事件路由到控制器,都将使用正确的控制器调用
__call__
方法。Give each event a method (possibly even using
__call__
), and pass in the Controller object as an argument. The "call" method should then call the controller object. For example...Whatever code you're using to route your events to your controllers will call the
__call__
method with the correct controller.我偶然发现了同样的问题(几乎十年后!),这是我一直在使用的实现,以便 EventManager 仅通知侦听器的子集。
它基于
defaultdict
:EventManager 的_listeners
属性是WeakKeyDictionary()
的defaultdict
。事件都是继承自一个空的抽象
Event
类,因此侦听器可以只关注他们想要侦听的某些事件类。下面是一个简单的代码来了解其背后的想法:
注册时,侦听器告诉事件管理器它想要通知哪种事件类型。
发布事件时,事件管理器只会通知注册为该类型事件通知的侦听器。
当然,这段代码的范围比 @Paul Manta 提出的非常通用(而且非常优雅)的解决方案要小得多,但就我而言,它有助于消除对
isinstance
和其他的一些重复调用检查的同时尽可能保持事情简单。这样做的缺点之一是所有类型的事件都必须是某个类的对象,但在 OO python 中,这应该是正确的方法。
I stumbled upon the same issue (almost a decade later!), and here is an implementation I've been using so that the EventManager would notify only a subset of listeners.
It is based on
defaultdict
: the_listeners
attribute of EventManager is adefaultdict
ofWeakKeyDictionary()
.Event are all inherited from an empty abstract
Event
class, so listeners can focus only on some classes of events they want to listen to.Here is a minimalist code to get the idea behind it:
When registering, a listener tells the event manager which event type it wants to be notified of.
When posting events, the event manager will only notify the listeners which registered to be notified for that type of event.
This piece of code has, of course, much less scope than the very general (and very elegant) solution proposed by @Paul Manta, but in my case, it helped remove some repetitive calls to
isinstance
and other checks while keeping things as simple as I could.One of the drawbacks of this is that all type of events have to be objects of some class, but in OO python, this is supposed to be the way to go.