Combining dict in super class's init and subclass's init automatically?

125 Views Asked by At

I'm creating an event system which uses the following class for events:

class Event(set):
    def __init__(self, name, iterable=()):
        super().__init__(iterable)
        self.name = name

    def __iadd__(self, listener):
        self.add(listener)
        return self

    def __isub__(self, listener):
        self.remove(listener)
        return self

    def fire(self, **eargs):
        for listener in self:
            listener(**eargs)

Now I'm trying to create some kind of a dict that would automatically create the events in its __init__ like so:

class EventDict(dict):
    def __init__(self, prefix, *event_names):
        super().__init__({
            name: Event('%s.%s' % (prefix, name))
            for name in event_names
        })

And here's an example of usage:

class Player:
    def __init__(self, name):
        self._name = name
        self.events = EventDict('Player', 'change_name')

    @property
    def name(self):
        returns self._name

    @name.setter
    def name(self, value):
        old_name = self.name
        self.name = value
        self.events['change_name'].fire(player=self, old_name=old_name)

Now the problem I'm facing is subclassing. If I were to subclass my Player class to include also health attribute, I can't use the same way of creating an event dict, cause it would override the existing one and I couldn't access change_name anymore.

So I'm trying to find a way where I can just do something like this (ideal solution):

class Player:
    events = EventDict('Player', 'change_name')

class Player2(Player):
    events = EventDict('Player2', 'attack', 'kill')

p2 = Player2()
p2.events['change_name'] += my_event_listener  # Still access Player class's events

Would something like this be possible?


I know I can do:

class Player2(Player):
    def __init__(self, name):
        super().__init__()
        self.events.update(...)

But it's not the same :P

2

There are 2 best solutions below

4
On BEST ANSWER

Stop using EventDict. The class itself has its own dict which supports inheritance like that.

class Player:
    def __init__(self, name):
        self._name = name
        self.change_name_event = Event('Player.change_name')

class Player2(Player):
    def __init__(self, name):
        super().__init__(name)
        self.attack_event = Event('Player2.attack')
        self.kill_event = Event('Player2.kill')

All the events from the subclasses will be added no matter what.

I noticed that maybe you wanted to make it obvious that they're events, so I added 'event' to the names of the fields, but you don't need to if you don't want to.

If you wanted it so that the prefix is the same throughout, then you'd change the strings from something like 'Player.change_name' to self.__class__.__name__ + '.change_name'. That way, it always gets whatever the actual class for the object is. This is part of what @jonrsharpe's solution is trying to get at.

If you wanted to make it so others could add more events dynamically, they can simply do a line like playerObj.my_new_event = Event('Player.my_new_event') or you could provide a nice method in the Player class to make their lives easier:

def add_event(self, event_name):
    setattr(self, event_name, Event(self.__class__.__name__ + '.' + event_name)
3
On

I think what you want is:

class Player:

    EVENTS = ('change_name',)

    def __init__(self, name):
        self._name = name
        self.events = EventDict(
            self.__class__.__name__, 
            *self.EVENTS,
        )

    ...

Then all you need in Player2 is:

class Player2(Player):

    EVENTS = Player.EVENTS + ('attack', 'kill')

and the inherited __init__ will work fine.