William George
William George

Reputation: 35

Python: selectively overloading attributes?

So this is the sort of thing I'm looking to accomplish

class Class(object):
    _map = {0: 'ClassDefault', 1:'3', 2:'xyz'}

    def handle_map(self):
        print('.'.join((self._map[0], self._map[1], self._map[2])))


class SubClass(Class):
    _map = {3: '012'}


class SubSubClass(SubClass):
    _map = {0: 'SubClassDefault', 4: 'mno'}

    def handle_map(self):
        print('.'.join((self._map[0], self.map[4])).upper())
        super().handle_map()

c, sc, ssc = Class(), SubClass(), SubSubClass()

c.handle_map()
# ClassDefault.3.xyz
sc.handle_map()
# ClassDefault.3.xyz (_map[3] is used by some other method entirely, etc.)
ssc.handle_map()
# SUBSUBCLASSDEFAULT.MNO
# 3.xyz  --OR-- SubSubClassDefault.3.xyz

So essentially what I want to do is have an easy way to define values in a parent and it's subclasses. I want that data to be handled, by default, at the level in the hierarchy that defined it, because subclasses shouldn't have to, but I also want the subclasses to have the option of either overriding that handling altogether or at least modifying it before it goes up the chain.

I'm not completely hung up on _map being a dictionary. It could be a set of objects, or even just tuples, etc... The clearest example I can give for a use case would be to generate signatures for init(). So many (but not all) of the parameters would be common and I want to avoid using the same boiler plate over and over again. At the same time, sensible defaults for the parent class are not sensible for the subclasses. They should be able to override those defaults.

Likewise, a subclass might have a different way it needs to interpret a given value, or not, so it shouldn't be obligated to entirely redefine how it handles them if only some slight modification is necessary.

I've got some half-formed ideas of how this could or should be implemented, but nothing has come all the way together yet. I got quite long way into implementing a metaclass to do this, but had difficulty getting the parent classes methods to ignore the values defined/handled by subclasses. As I typed this up I considered using collections.chainmap to replace a bunch of the code I wrote in the metaclass, but that doesn't yet solve any of the problems I've ran into, I think.

So my question is what is the most sensible existing pattern for this? Alternatively, is this simply not a feasible thing to ask for?

Upvotes: 3

Views: 76

Answers (2)

Ethan Furman
Ethan Furman

Reputation: 69031

The key question is: What happens when _map changes in a parent class?

  • subclasses should see the change --> use something along the lines of @noahbkim's answer

  • subclasses should not see the change --> use either a class decorator or a metaclass to cement the created class.

A metaclass solution looks like this:

class ClassMeta(type):
    def __new__(metacls, cls, bases, classdict):
        new_class = super(ClassMeta, metacls).__new__(metacls, cls, bases, classdict)
        _map = {}
        prev_classes = []
        obj = new_class
        for base in obj.__mro__:
            if isinstance(base, ClassMeta):
                prev_classes.append(base)
        for prev_class in reversed(prev_classes):
            prev_class_map = getattr(prev_class, '_map', {})
            _map.update(prev_class_map)
        new_class._map = _map
        return new_class

class Class(object, metaclass=ClassMeta):
    ...

Upvotes: 1

noahbkim
noahbkim

Reputation: 538

Alright, I think I have a solution:

class Class:
    _map = {0: "a", 1: "b"}

    def getmap(self):
        # Create an empty map
        mymap = {}
        # Iterate through the parent classes in order of inheritance
        for Base in self.__class__.__bases__[::-1]:
            # Check if the class has a "getmap" attribute
            if issubclass(Base, Class):
                # Initialize the parent class
                b = Base()
                # If so, add its data to mymap
                mymap.update(b.getmap())
        # Finally add the classes map data to mymap, as it is at the top of the inheritance
        mymap.update(self._map)
        return mymap

class SubClass(Class):
    _map = {2: "c"}

class SubSubClass(SubClass):
    _map = {0: "z", 3: "d"}

c = Class()
sc = SubClass()
ssc = SubSubClass()

print(c.getmap())  # -> {0: 'a', 1: 'b'}
print(sc.getmap())  # -> {0: 'a', 1: 'b', 2: 'c'}
print(ssc.getmap())  # -> {0: 'z', 1: 'b', 2: 'c', 3: 'd'}

As far as I can tell this does everything you need. Let me know if you have questions.

Upvotes: 1

Related Questions