3
\$\begingroup\$

My code implements the following structure :Module contains Submodule contains Ability contains Template. There is so much repetition that I'm sure there is a better way.

class Module:
    def __init__(self, name, lang):
        assert lang in ['fr', 'en']
        self.name = name
        self.lang = lang
        self.submodules = {}

    def add_submodule(self, submodule):
        assert isinstance(submodule, Submodule)
        assert submodule.name not in self.submodule
        self.submodules[submodule.name] = submodule


class Submodule:
    def __init__(self, name, module):
        self.name = name
        self.link_with(module)
        self.abilities = {}

    def add_ability(self, ability):
        assert isinstance(ability, Ability)
        assert ability.name not in self.abilities
        self.abilities[ability.name] = ability

    def link_with(self, module):
        assert isinstance(module, Module)
        self.module = module
        module.add_submodule(self)


class Ability:
    def __init__(self, name, submodule):
        self.name = name
        self.link_with(submodule)
        self.templates = {}

    def add_template(self, template):
        assert isinstance(template, Template)
        assert template.name not in self.templates
        self.templates[template.name] = template

    def link_with(self, submodule):
        assert isinstance(submodule, Submodule)
        self.submodule = submodule
        submodule.add_ability(self)


class Template:
    def __init__(self, name, ability):
        self.name = name
        self.link_with(ability)
        self.lang = ability.submodule.module.lang

    def link_with(self, ability):
        assert isinstance(ability, Ability)
        self.ability = ability
        ability.add_template(self)

I could have coded a global function "link_to_parent" and set 'childs' sets and 'parent' set but I prefer to be able to do : ability.submodule.module instead of doing ability.parent.parent which is not explicit enough.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ If you added how you were using it then we could give a much better review. Currently I'd recommend some weird god dict. \$\endgroup\$ – Peilonrayz May 5 '17 at 20:37
  • \$\begingroup\$ And what is a module, submodule and template? You're not really explaining your context about why you need this. Please see this: codereview.meta.stackexchange.com/a/6429/31562 \$\endgroup\$ – Simon Forsberg May 6 '17 at 19:18
1
\$\begingroup\$

YOU ARE IN A MAZE OF TWISTY LITTLE CLASSES, ALL ALIKE

So when seeing a bunch of classes that are all very similar, I start to think Metaclass! So let's give that a whirl.

Below I will discuss a refactor of your implementation using a metaclass. The metaclass will allow us to implement the parent/child relationships generically, but then also gives us ready access to the same infrastructure using names which are not parent/child

What are metaclasses?

Metaclasses are classes that create classes. This in the same relationship wherein classes are used to create objects. Take for instance this class declaration:

# Static class declaration
class AClass(int):

    a_class_attribute = 'twisty'

The above can be equivalently declared by calling type directly as:

# Dynamic (meta) declaration        
AClass = type('AClass', (int,), {'a_class_attribute': 'twisty'}).

And in this case type is the metaclass of AClass, since it is called to make a class.

I won't go any farther in this explanation because it can go on for a while, and there are lot's of resources to read on this topic.

Removing Common Code

So the data structure under discussion consists of 4 classes that are hierarchical as

  • class Module: a container for instances of:
  • class Submodule: a container for instances of:
  • class Ability: a container for instances of:
  • class Template:

The code then consists of various boiler plate for the management of the tree. Let's refactor the various boilerplate into a metaclass, and a base class, to remove the common code.

Metaclass as a class

Our metaclass inherits from type and has the same signature:

class ModuleMetaClass(type):

    def __init__(cls, name, bases, attrs):

This __init__ method will be called every time we declare one of our classes. In this __init__, we do the bookkeeping necessary to relate the classes to one another. Basic idea is that we will declare our classes in the order they appear in the hierarchy. Thus the first declaration will be for Module, and the second for Submodule, etc. Then in the ModuleMetaClass.__init__ we can relate the classes to each other as parent/child.

__init__ starts with:

if cls._metaclass_instances is None:
    # First instance is ModuleBaseClass
    cls._parent_class = None
    cls._metaclass_instances = [type(None)]

here, we initialize the list of classes that will be instantiated, and note that this class being at the top of the tree has no parent. Continuing...

else:
    # parent class is the previously declared class
    cls._parent_class = cls._metaclass_instances[-1]

    # if not at the top of the tree, then we are our parent's child
    if cls._parent_class != type(None):
        cls._parent_class._child_class = cls

    # store this class in the list of classes
    cls._metaclass_instances.append(cls)

will note that our parent class is at the end of the class list (ie: last added), then if we have a parent, register us as his child, and then add ourselves to the end of the class list.

And that is basically it for the metaclass. There is a bit more bookkeeping, but see the full listing at the bottom of the post for that.

A Base Class to manage the tree relationships

The first instance of our metaclass (ModuleMetaClass) will be a base class for each of the node types. Class starts with:

class ModuleBaseClass(metaclass=ModuleMetaClass):
    """ Base class for each of the derived classes in our tree """

This indicates that our class will be built by instantiating a ModuleMetaClass. ModuleBaseClass.__init__() contains the boilerplate from the original data structure and the meta bits to allow our named attributes. The fun bits start with:

# child class variable plural is used to add a common name
plural = getattr(self._child_class, '_plural')
if plural is not None:
    setattr(self, plural, self._children)

This code uses the information stored in the class hierarchy from the metaclass to add an access attribute to this node's children. Similarly:

# add an access attribute for each of the nodes above us in the tree
while parent is not None:
    setattr(self, type(parent).__name__.lower(), parent)
    parent = parent._parent

adds an access attribute to our parent node, and his parent and... all the way to the top of the tree. The original implementation only had an accessor for the immediate parent.

Declaring our classes

So the remaining code contains the operative elements of the classes themselves, with all of the common boilerplate removed to ModuleBaseClass and ModuleMetaClass.

class Module(ModuleBaseClass):
    def __init__(self, name, lang):
        super().__init__(name, None)
        assert lang in ['fr', 'en']
        self.lang = lang

class Submodule(ModuleBaseClass):
    _plural = 'submodules'

class Ability(ModuleBaseClass):
    _plural = 'abilities'

class Template(ModuleBaseClass):
    _plural = 'templates'

    def __init__(self, name, ability):
        super().__init__(name, ability)
        self.lang = module.lang

Note the last line where Template was able to reach all the way up to Module.lang to retrieve the language.

Full Code Listing

class ModuleMetaClass(type):
    """ Metaclass that is instantiated once for each class """
    _metaclass_instances = None

    def __init__(cls, name, bases, attrs):

        if cls._metaclass_instances is None:
            # First instance is ModuleBaseClass
            cls._parent_class = None
            cls._metaclass_instances = [type(None)]
        else:
            # parent class is the previously declared class
            cls._parent_class = cls._metaclass_instances[-1]

            # if not at the top of the tree, then we are our parent's child
            if cls._parent_class != type(None):
                cls._parent_class._child_class = cls

            # store this class in the list of classes
            cls._metaclass_instances.append(cls)

        # no child class yet
        cls._child_class = None

        # call our base (meta) class init
        super().__init__(name, bases, attrs)


class ModuleBaseClass(metaclass=ModuleMetaClass):
    """ Base class for each of the derived classes in our tree """

    def __init__(self, name, parent):
        assert isinstance(parent, self._parent_class)
        self.name = name
        self._parent = parent

        if self._child_class is not None:
            self._children = {}

            # child class variable plural is used to add a common name
            plural = getattr(self._child_class, '_plural')
            if plural is not None:
                setattr(self, plural, self._children)

        # add self to our parents collection
        if parent is not None:
            parent._add_child(self)

        # add an access attribute for each of the nodes above us in the tree
        while parent is not None:
            setattr(self, type(parent).__name__.lower(), parent)
            parent = parent._parent

    def _add_child(self, child):
        assert isinstance(child, self._child_class)
        assert child.name not in self._children
        self._children[child.name] = child

# --------------------------------

class Module(ModuleBaseClass):
    def __init__(self, name, lang):
        super().__init__(name, None)
        assert lang in ['fr', 'en']
        self.lang = lang

class Submodule(ModuleBaseClass):
    _plural = 'submodules'

class Ability(ModuleBaseClass):
    _plural = 'abilities'

class Template(ModuleBaseClass):
    _plural = 'templates'

    def __init__(self, name, ability):
        super().__init__(name, ability)
        self.lang = module.lang

# --------------------------------

module = Module('module1', 'fr')
sub_module1 = Submodule('sub_module1', module)
sub_module2 = Submodule('sub_module2', module)
ability = Ability('ability1', sub_module1)
template = Template('template1', ability)
print(template.lang)
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.