Python Duck-Typing для обработки событий MVC в Pygame - PullRequest
7 голосов
/ 31 августа 2011

Мы с другом играли с Pygame и наткнулись на это руководство по созданию игр с использованием Pygame. Нам очень понравилось, как игра превратилась в систему контроллера модели-вида с событиями в качестве посредника, но код делает тяжелым использование isinstance проверок для системы событий.

Пример:

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

Это приводит к некоторому крайне непитонному коду. У кого-нибудь есть предложения о том, как это можно улучшить? Или альтернативная методология реализации 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()

Ответы [ 3 ]

12 голосов
/ 03 сентября 2011

Более чистый способ обработки событий (и также намного более быстрый, но, возможно, потребляющий немного больше памяти) состоит в том, чтобы в вашем коде было несколько функций-обработчиков событий.Что-то вроде этого:

Желаемый интерфейс

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, который содержит толькослабая ссылка на self (я вижу, вы уже знаете о слабых ссылках), так что EventDispatcher не предотвращает удаление self.

Альтернативой может быть функция 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 Objects (Необязательно))

При удалении слушателей из диспетчера / диспетчера вместо выполнения 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))
        ]

Здесь EventDispatcher.add возвращает объект Connection, который знает, где в списке EventDispatcher он находится.Когда объект NotifyThisClass удален, то же самое происходит и с self._connections, который вызовет Connection.__del__, что приведет к удалению слушателя из EventDispatcher.

. Это может сделать ваш код более быстрым и простым в использовании.поскольку вам нужно только явно добавить функции, они удаляются автоматически, но вам решать, хотите ли вы это сделать.Если вы сделаете это, обратите внимание, что EventDispatcher.remove больше не должно существовать.

2 голосов
/ 03 сентября 2011

Я наткнулся на учебник С.Дж. Брауна по созданию игр в прошлом. Это отличная страница, одна из лучших, которые я читал. Однако, как и вам, мне не понравились призывы к isinstance или тот факт, что все слушатели получают все события.

Во-первых, isinstance медленнее, чем проверка того, что две строки равны, поэтому я закончил тем, что сохранил имя в своих событиях и проверил имя, а не класс. Но, тем не менее, функция уведомления с батареей , если зудила меня, потому что это было пустой тратой времени. Мы можем сделать две оптимизации здесь:

  1. Большинство слушателей интересуются только несколькими типами событий. По соображениям производительности, когда публикуется QuitEvent, должны быть уведомлены только заинтересованные в нем слушатели. Менеджер событий отслеживает, какой слушатель хочет прослушать какое событие.
  2. Тогда, чтобы избежать прохождения тонны , если операторов в одном методе notify , у нас будет один метод для каждого типа события.

Пример:

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

Поскольку я хочу, чтобы разработчик печатал как можно меньше, я сделал следующее:

Когда слушатель регистрируется в менеджере событий, менеджер событий сканирует все методы слушателя. Когда один метод начинается с 'on' (или любого другого префикса, который вам нравится), он смотрит на остальные ("QuitEvent") и привязывает это имя к этому методу. Позже, когда менеджер событий качает свой список событий, он смотрит на имя класса событий: «QuitEvent». Он знает это имя и поэтому может напрямую вызывать все соответствующие обработчики событий. Разработчику нечего делать, кроме добавления методов onWhwhatEvent, чтобы они работали.

У него есть некоторые недостатки:

  1. Если я сделаю опечатку на имя обработчика ("onRunPhysicsEvent" вместо "onPhysicsRanEvent" например "), тогда мой обработчик будет никогда не звони, и я удивлюсь, почему. Но я знаю хитрость, поэтому я не удивляюсь, почему очень долго.
  2. Я не могу добавить обработчик событий после того, как слушатель был зарегистрировано. Я должен отменить регистрацию и перерегистрацию. Действительно, обработчики событий проверяются только при регистрации. затем опять же, мне никогда не приходилось этого делать, поэтому я не скучаю по нему.

Несмотря на эти недостатки, мне это нравится гораздо больше, чем то, что конструктор слушателя явно объясняет менеджеру событий, что он хочет следить за этим, этим, этим и этим событием. И в любом случае это та же скорость выполнения.

Второй пункт:

При разработке нашего менеджера мероприятий мы хотим быть осторожными. Очень часто слушатель реагирует на событие, создавая-регистрируя или отменяя регистрацию-уничтожая слушателей. Это происходит все время. Если мы не подумаем об этом, наша игра может сломаться с RuntimeError: размер словаря изменился во время итерации . Код, который вы предлагаете, перебирает копию словаря, поэтому вы защищены от взрывов; но это имеет последствия, о которых нужно знать: - Слушатели, зарегистрированные из-за события, не получат это событие. - Слушатели, не зарегистрированные из-за события, все равно получат событие. Я никогда не находил это проблемой, хотя.

Я сам реализовал это для игры, которую разрабатываю. Я могу связать вас с двумя с половиной статьями, которые я написал на эту тему:

Ссылки на мою учетную запись github приведут вас непосредственно к исходному коду соответствующих частей. Если вы не можете ждать, вот что: https://github.com/Niriel/Infiniworld/blob/v0.0.2/src/evtman.py. Там вы увидите, что код моего класса событий немного велик, но каждое унаследованное событие объявляется в 2 строки: базовый класс Event облегчает вашу жизнь.

Итак, все это работает с использованием механизма самоанализа python и того факта, что методы являются объектами, подобными любым другим, которые могут быть помещены в словари. Я думаю, что это довольно pythony:).

1 голос
/ 31 августа 2011

Дайте каждому событию метод (возможно, даже используя __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__ с правильным контроллером.

...