Search code examples
pythoninheritancefactory-pattern

How to achieve inheritance using classmethod factory functions


How do I use/reuse implementations in the parent class when using the classmethod approach for implementing factory functions?

In the example below, class A is fine, but class B is broken.

class A(object):
    def __init__(self, **kwds):
        self.__dict__.update(kwds)
    @classmethod
    def from_jdata(cls, data):
        if '_id' in data:
            data['uuid'] = data['_id']
            del data['_id']
        return cls(**data)

class B(A):
    def __init__(self, **kwds):
        super(B, self).__init__(**kwds)
    @classmethod
    def from_jdata(cls, data):
        # goal: make an instance of B, 
        # using the logic that is implemented in A.from_jdata
        # But does some extra stuff, akin to:
        res = A.from_jdata(B, data)
        res.__dict__['extra']='set'
        return res

The context is that I'm trying to instantiate instances based on JSON configuration data. The inheritance hierarchy is deeper than just two classes, i.e. there are a number of children of class B. The root of the inheritance hierarchy does some useful stuff in the factory function. Children classes should re-use that but add on some additional operations.


Solution

  • Use super, of course:

    class A(object):
        def __init__(self, **kwds):
            self.__dict__.update(kwds)
        @classmethod
        def from_jdata(cls, data):
            if '_id' in data:
                data['uuid'] = data['_id']
                del data['_id']
            return cls(**data)
    
    class B(A):
        def __init__(self, **kwds):
            super(B, self).__init__(**kwds)
        @classmethod
        def from_jdata(cls, data):
            # goal: make an instance of B,
            # using the logic that is implemented in A.from_jdata
            # But does some extra stuff, akin to:
            res = super().from_jdata(data)
            # res = super(B, cls).from_jdata(data) # in python 2
            res.__dict__['extra']='set'
            return res
    

    In action:

    In [6]: b = B.from_jdata({'_id':42, 'foo':'bar'})
    
    In [7]: vars(b)
    Out[7]: {'foo': 'bar', 'uuid': 42, 'extra': 'set'}
    

    Note, what you were trying to do won't work because @classmethod creates a descriptor that binds the class when called from either the class or the instance. You would have to access the raw function using something like:

    res = A.__dict__['from_jdata'].__func__(B, data)
    

    To make it work, but just use super, that's what it is for.