0

I'm trying to store specific actions that are defined within a class.

To reduce code duplication, I would like to make use of a mixin class that stores all the actions based on a decorator.

The idea is that it should be straightforward for other people to extend the classes with new actions. I especially want to avoid that these actions are explicitly listed in the source code (this should be handled by the decorator).

This is what I came up with. Unfortunately, in all .actions lists, all the actions from all the classes are listed.

However, I would like to have a solution that only the actions of the specific class are listed.

class ActionMixin:
    actions = []

    @staticmethod
    def action(fun):
        ActionMixin.actions.append(fun)
        return fun


class Human(ActionMixin):
    @ActionMixin.action
    def talk(self):
        pass


class Dog(ActionMixin):
    @ActionMixin.action
    def wuff(self):
        pass


class Cat(ActionMixin):
    @ActionMixin.action
    def miau(self):
        pass


if __name__ == "__main__":
    party = [Human(), Dog()]
    possible_actions = [action for memer in party for action in member.actions]
    # I would like that possible_actions is now only Human.talk() and Dog.wuff()
    # instead it is 2 times all actions
    print(len(possible_actions))  # == 6
user7431005
  • 3,899
  • 4
  • 22
  • 49

1 Answers1

2

I would just write my own descriptor here. So:

class Registry:
    def __init__(self):
        self._registered = []
    def __call__(self, func):
        self._registered.append(func)
        return func
    def __get__(self, obj, objtype=None):
        return self._registered


class Human:
    actions = Registry()
    @actions
    def talk(self):
        pass


class Dog:
    actions = Registry()
    @actions
    def wuff(self):
        pass


class Cat:
    actions = Registry()
    @actions
    def miau(self):
        pass

So, instead of inheriting from a mixin, just initialize the descriptor object. Then that object itself can be used as the decorator (the __call__ method!).

Note, the decorator would be whatever name you assigned it, and it would be the name of the attribute where the actions are stored.

In the REPL:

In [11]: party = [Human(), Dog()]

In [12]: [action for member in party for action in member.actions]
Out[12]: [<function __main__.Human.talk(self)>, <function __main__.Dog.wuff(self)>]

EDIT:

You would have to change the implementation if you want this to live in a base class. Basically, use a dict to keep track of the registries, unfortunately, we have to rely on the brittle __qualname__ to get the class in __call__:

class ActionsRegistry:
    def __init__(self):
        self._registry = {}
    def __call__(self, func):
        klass_name, func_name = func.__qualname__.rsplit('.', 1)
        if klass_name not in self._registry:
            self._registry[klass_name] = []
        self._registry[klass_name].append(func)
        return func
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self._registry[objtype.__qualname__]

class Base:
    actions = ActionsRegistry()


class Human(Base):
    @Base.actions
    def talk(self):
        pass


class Dog(Base):
    @Base.actions
    def wuff(self):
        pass


class Cat(Base):
    @Base.actions
    def miau(self):
        pass
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • that is a great solution and I'll accept it. One more follow-up question: In my application, all classes (Human, Dog, Cat) inherit from a Base class. Would it be possible to only assign `actions` in that base class and use the @actions decorator in the sub-classes as well? – user7431005 Feb 18 '21 at 13:49
  • @user7431005 make sure to `return func` from `__call__`, as to this latest requirement, I think maybe I can hack something up, it may not be pretty – juanpa.arrivillaga Feb 18 '21 at 14:16
  • 1
    @user7431005 ok, hacked something together, one slight snag - it can only retrieve the actual actions list on an instance, not a class – juanpa.arrivillaga Feb 18 '21 at 14:31
  • works like a charm - I'll give it a try in our project and see how it behaves long-term ;-) – user7431005 Feb 18 '21 at 14:34