Events / Eventhandler

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

Hallo.
Ich spiele gerne sinnlos mit Python rum und hab mir jetzt mal ein Eventsystem gebaut, das eben Events verwaltet.
Keine Events im Sinne von Partyevents, sondern Auslöser für irgendwelchen Programmcode ;)
Beispielsweise für ein Spiel, in dem für jede Aktion theoretisch ein Event ausgelöst werden könnte.
Wenn es rundenbasiert ist, würde also am Anfang jeder Runde ein Event namens "Tick" ausgelöst werden oder so.

Was haltet ihr von dem Code und was würdet ihr anders machen?

Code: Alles auswählen

class EventHandler():
    """ This class handles all events and triggers the assigned commands """
    def __init__(self):
        self.events = {'tick': [],
                       'sonstwas': [],
                       '...': []
                      }

    def add_event(self, event):
        if not isinstance(event, Event):
            raise TypeError('event must be of type event')
        
        if isinstance(event, TickEvent):
            self.events['tick'].append(event)

        # elif ...

        else:
            raise TypeError('Event type is not implemented') 

    def tick(self):
        for event in self.events['tick']:
            if event.duration == 0:
                for func in event.functions:
                    func()
            event.duration -= 1


class Event():
    """ This is the base class for all events """
    def __init__(self):
        self.functions = []

    def bind_function(self, function):
        if hasattr(function, '__call__'):
            self.functions.append(function)
        else:
            raise TypeError()


class TickEvent(Event):
    """ This Event will be triggered after a certain amount of ticks """
    def __init__(self, duration):
        super().__init__()
        self.duration = duration

# ---- Beispiel ---- #
def bla():
    print ('ausgelöst')

handler = EventHandler()
test = TickEvent(14)
test.bind_function(bla)
handler.add_event(test)
for i in range(15):
    print (i)
    handler.tick()
Mir ist klar, dass es noch einige Kleinigkeiten zu verbessern gibt. zB lösche ich die TickEvents nicht, sondern zähle sie in den Minusbereich weiter, aber darauf kommt's ja erst mal nicht an.
Wie würdet ihr es machen bzw was würdet ihr verändern?

Danke schon mal für eure Antworten :)
BlackJack

@Nocta: Ich finde die `isinstance()`-Tests und das mit dem Test auf `__call__` "unpythonisch", bzw. das mit dem `isinstance()` auch un"OOP".

Für's Aufrufen sollte das Event zuständig sein, und nicht der Handler.

Die Funktionen würde ich in einem `set()` speichern, statt in einer Liste.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

Danke für deine Antwort.
@Nocta: Ich finde die `isinstance()`-Tests und das mit dem Test auf `__call__` "unpythonisch", bzw. das mit dem `isinstance()` auch un"OOP".
Sorry, aber was meinst du mit dem Zusatz un"OOP"? (Vielleicht irgendwo verrutscht?)
Und was schlägst du vor, anstelle der isinstance()-Tests? Einfach weglassen oder das ganze komplett anders angehen?
Für's Aufrufen sollte das Event zuständig sein, und nicht der Handler.
Der Handler sollte ja genau diese Aufgabe erfüllen :D
Gehen wir noch mal auf den möglichen Einsatz des Systems in einem Computerspiel ein.
Eine Einheit könnte 4 Runden (Ticks) lang vergiftet sein.
Dann würde ich ein Event erstellen, welches der Einheit jede Runde 10 HP abzieht und dieses Event dem EventHandler übergeben, der dann jede Runde alle TickEvents auslöst, die in der Liste sind.
Wie würde das ohne einen solchen EventHandler gehen?

(Edit: Wobei ich das mit der `event.duration` etwas verpeilt habe, eigentlich müsste Zeile 23 als Operator `>` oder `>=` statt `==` verwenden)

Die Funktionen würde ich in einem `set()` speichern, statt in einer Liste.
Okay, stimmt eigentlich ;)
Zuletzt geändert von Nocta am Samstag 13. März 2010, 23:52, insgesamt 1-mal geändert.
BlackJack

@Nocta: ``if``/``elif``\s mit Typtests wo dann pro Typ etwas anderes gemacht wird, ist bei OOP ein Code-Smell. Das hat man "vor" OOP gemacht und das hat halt Nachteile. Zum Beispiel das man für jeden neue Typ all diese Tests in Programmen suchen und um den neuen Typ erweitern muss.

Es gibt da ja noch nicht soviel Code, aber es scheint Du willst an der Stelle je nach Typ in eine andere Liste speichern. Das könnte man zum Beispiel mit einem `TYPE`-Attribut auf der konkreten Event-Klasse lösen und im "Handler" dann nur noch folgendes schreiben:

Code: Alles auswählen

    def add_event(self, event):
        self.events[event.TYPE].append(event)
Ich meinte nicht das der EventHandler weg soll, nur dass die Events für das Aufrufen ihrer Funktionen selber zuständig sind. Die Schleife die konkret die Funktionen des Events aufruft sollte auch *im* Event stehen und von Handler sollte nur eine entsprechende Methode aufgerufen werden. Ich würde dafür `__call__()` verwenden -- dann kann man Events als "Funktionen" an Events binden und so kaskadieren.

Das Runterzählen der "Ticks" könnte man auch in das `TickEvent` verlagern. Dann muss der Handler da kein grosses Spezialwissen mehr haben.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

BlackJack hat geschrieben:@Nocta: ``if``/``elif``\s mit Typtests wo dann pro Typ etwas anderes gemacht wird, ist bei OOP ein Code-Smell. Das hat man "vor" OOP gemacht und das hat halt Nachteile. Zum Beispiel das man für jeden neue Typ all diese Tests in Programmen suchen und um den neuen Typ erweitern muss.
Ja das stimmt. Das ist mir auch bei'm Programmieren aufgefallen aber ich hatte auf die Schnelle keine bessere Idee :)
Aber wegen solchen Sachen stell ich den Code ja hier vor.
Ich meinte nicht das der EventHandler weg soll, nur dass die Events für das Aufrufen ihrer Funktionen selber zuständig sind. Die Schleife die konkret die Funktionen des Events aufruft sollte auch *im* Event stehen und von Handler sollte nur eine entsprechende Methode aufgerufen werden. Ich würde dafür `__call__()` verwenden -- dann kann man Events als "Funktionen" an Events binden und so kaskadieren.
Der erste Teil leuchtet mir ein, aber ich versteh nicht, was du mit dem letzten Satz meinst.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

Ich habe den Code mal überarbeitet und dank deiner Verbesserungsvorschläge kommt mir das ganze schon viel besser vor, danke! :)
Der EventHandler ist jetzt beispielweise komplett unabhängig von der konkreten Implementierung des Events und ruft einfach nur die __call__ Methode auf.

Code: Alles auswählen

class EventHandler():
    """ This class handles all events and triggers the assigned commands """
    def __init__(self):
        self.events = {'Tick': [],  # this event is triggered every tick
                       'sonstwas': [],
                       '...': []
                      }

    def add_event(self, event):
        if not isinstance(event, Event):
            raise TypeError('event must be of type event')
        self.events[event.type].append(event)

    def tick(self):
        for etype in self.events:
            for event in self.events[etype]:
                event()
                

class Event():
    """ This is the base class for all events """
    def __init__(self):
        self.type = 'event'
        self.functions = set()

    def bind_function(self, function):
        self.functions.add(function)

    def __call__(self):
        for func in self.functions:
            func()


class TickEvent(Event):
    """ This Event will be triggered for a certain amount of ticks """
    def __init__(self, duration):
        super().__init__()
        self.type = 'Tick'
        self.duration = duration

    def __call__(self):
        if self.duration > 0:
            for func in self.functions:
                func()
        self.duration -= 1

        

def bla():
    print ('ausgelöst')

handler = EventHandler()
test = TickEvent(4)
test.bind_function(bla)
handler.add_event(test)
for i in range(15):
    print (i)
    handler.tick()
Aber was haltet ihr eigentlich von super()? (Zeile 37)
Das kommt mir zwar irgendwie unnötig vor, aber wenn ich ja sowieso alle Methoden komplett überschreibe, brauch ich eigentlich gar nicht mehr von Event zu erben.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo.

Was genau möchtest du mit den Zeilen 4-7 erreichen? Sind die hier nur beispielhhaft oder möchtest du hier alle möglichen Events aufzählen? Letzteres wäre nämlich sehr unflexibel.

Zeilen 10 und 11 solltest du einfach wegschmeißen, das ist eine unnötige Einschränkung. Ich werfe dir einfach mal das Stichwort Ducktyping zu ;-)

Zeilen 15 bis 17 würde ich so lösen:

Code: Alles auswählen

for events in self.events.values():
    for event in events:
        event()
Das "type" ein reservierte Name ist, ist dir wahrscheinlich auch schon aufgefallen ^^

Bis dann,
Sebastian
Das Leben ist wie ein Tennisball.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

Danke auch für deine Antwort.
EyDu hat geschrieben: Was genau möchtest du mit den Zeilen 4-7 erreichen? Sind die hier nur beispielhhaft oder möchtest du hier alle möglichen Events aufzählen? Letzteres wäre nämlich sehr unflexibel.
Beim Ändern nach BlackJacks "Anleitung" hab ich an die Zeilen gar nicht mehr gedacht ;)
Ich hab das mal geändert und jetzt sollte viel flexibler sein.
Damit ich nicht den ganzen Thread überflüte: http://paste.pocoo.org/show/189345/
Zeilen 10 und 11 solltest du einfach wegschmeißen, das ist eine unnötige Einschränkung. Ich werfe dir einfach mal das Stichwort Ducktyping zu
Erledigt ;) Ich kann schon was mit Ducktyping anfangen, nur hab ich wohl noch den "Kontrollwahn" von anderen Sprachen.
Ich will halt immer bei jeder "Eingabe" direkt überprüfen, ob das sinnvoll ist.
Und ich denk mir halt: Besser ich werf die Exception jetzt und weiß auch genau, dass sie hier auftritt, als dass ich sie später behandeln muss.

Das "type" ein reservierte Name ist, ist dir wahrscheinlich auch schon aufgefallen ^^
Ja, aber ich hab's ja legitim benutzt, da self.type wohl was anderes als type ist ;) Aber ich hab's trotzdem mal in etype umbenannt, damit ich auch etype als Variablenname (ohne self) benutzen kann

Edit:
Jetzt hab ich aber 2 Probleme:
1. Wie kann ich am besten auch noch Parameter für die Funktionen mit übergeben? Wenn ich ein Tupel das sich aus *args ergibt, übergebe, krieg ich das Ding nicht wieder in Parameter entpackt.
2. Wie kann ich wieder etwas an das Programm zurückgeben, wenn ich die Funktionen aufrufe? Normalerweise gibt man ja per return irgendetwas zurück, aber das geht ja schlecht, wenn die Eventklasse die Funktionen aufruft.

Das ist doch nicht ganz so simpel, wie ich am Anfang dachte. Aber dank euch bin ich ja schon mal ein Stück weiter (vor allem qualitativ) als am Anfang
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo
Nocta hat geschrieben:Jetzt hab ich aber 2 Probleme:
1. Wie kann ich am besten auch noch Parameter für die Funktionen mit übergeben? Wenn ich ein Tupel das sich aus *args ergibt, übergebe, krieg ich das Ding nicht wieder in Parameter entpackt.
2. Wie kann ich wieder etwas an das Programm zurückgeben, wenn ich die Funktionen aufrufe? Normalerweise gibt man ja per return irgendetwas zurück, aber das geht ja schlecht, wenn die Eventklasse die Funktionen aufruft.

Das ist doch nicht ganz so simpel, wie ich am Anfang dachte. Aber dank euch bin ich ja schon mal ein Stück weiter (vor allem qualitativ) als am Anfang
Zu 1: Ich weiß nicht genau, ob ich dein Problem richtig erkannt habe, aber ein Tupel "spam" kannst du mit "*spam" wieder in Parameter umwandeln. Da du das aber wahrscheinlich weißt, schlage ich mal "functools.partial" vor.

Zu 2: Liefer doch einfach ein Tupel aller Rückgabewerte zurück. Wenn die Events asynchron ablaufen, da könntest du queue.Queue benutzen.

Sebastian
Das Leben ist wie ein Tennisball.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

EyDu hat geschrieben: Zu 1: Ich weiß nicht genau, ob ich dein Problem richtig erkannt habe, aber ein Tupel "spam" kannst du mit "*spam" wieder in Parameter umwandeln.
Oh man, wie dumm :D
Danke, das hab ich eigentlich gesucht.
Ich dachte das klappt nur auf umgekehrte Weise.
Es geht einfach darum, dass ich im Moment nur die Funktion übergebe, aber keine Parameter.

EyDu hat geschrieben:Zu 2: Liefer doch einfach ein Tupel aller Rückgabewerte zurück.
Das hab ich mir auch schon überlegt, aber das kam mir recht umständlich vor. Aber vielleicht geht's auch nicht einfacherer.

Danke, ich werd mich mal an die Arbeit ranmachen :)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Tupel sind die übliche Vorgehensweise, wenn man mehrere Werte zurückgeben möchte. Woran ich vorhin nicht gedacht habe ist, dass du eine Menge an Funktionen hast. Damit ist die Reihenfolge der aufgerufenen Funktionen nicht immer die selbe. Falls diese Zuordnung wichtig ist, würde ich als Rückgabewert ein Dictionary vorschlagen, mit der Funktion als Schlüssel und dessen Ergebnis als Wert. Auf diesem Weg sparst du dir auch das entpacken der Tupel oder das mitschleifen der Indizes.

Bis dann
Sebastian
Das Leben ist wie ein Tennisball.
Nocta
User
Beiträge: 290
Registriert: Freitag 22. Juni 2007, 14:13

Ja Tupel oder andere listenähnlichen Typen sind üblich.

Aber ich find das total unpraktisch in diesem Fall.
Ich versuch's mal schematisch irgendwie darzustellen:

Code: Alles auswählen

Main   ------>    EventHandler -------->  Event -------> Function(args) ------------------> Main
       ruft auf	             ruft auf        ruft auf                 Verändert Objekte
Also das Hauptprogramm kümmert sich nur um den EventHandler, welcher sich um die Events kümmert, welche sich selbst darum kümmern, dass die Funktionen aufgerufen werden.
Jetzt müssen die Auswirkungen der Funktionen aber irgendwie nach zurück nach "Main", um dort die Objekte zu verändern oder sonstwas zu machen.

Das ist mit Tupeln und Dictionaries aus Rückgabewerten glaube ich ziemlich chaotisch.
Mir hat mal jemand das Stichwort Observer (Entwurfsmuster) genannt. (Die Link-Funktion in diesem Board sollte mal überarbeitet werden, dann könnt ich's auch direkt verlinken - Jedenfalls Wikipedia)

Ich glaube aber, dass das nicht ganz so gut für den Fall geeignet ist (ich kann mich auch irren, ich kenn mich da ja nicht so aus), aber die Idee fand ich gut, dass man ein Objekt hat, das irgendwie als Schnittstelle dient, um durch die Funktionen Einfluss auf die Objekte im Hauptprogramm zu haben.

Ich kann zwar auch direkt in den Funktionen auf die Objekte zugreifen (wie ich in meinem anderen Thread bestaunt habe), aber das ist denke ich etwas zu unsauber und auch unflexibel.

Hat in dieser Richtung noch jemand einen Gedankenanstoß für mich?
Ansonsten muss ich's echt mal über Tupel/Dictionaries probieren.

Edit:
Ihr könnt natürlich auch gerne Alternativvorschläge für ein komplett anderes Programmdesign machen, vielleicht verrenn' ich mich gerade in irgendwas ..
Antworten