Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python: simulating the multiple inheritance of class variables

I have a class hierarchy where some methods work with a list of properties defined at the class level.

Let's say that for class A I have A.X = [propA1, propA2] and for subclass C I need C.X = [propA1, propA2, propC]. Subclasses inherit the properties from the parents, so it would make sense to write class methods with super() calls, each one using the properties of their own class.

However, it is a bit cumbersome. I can deal with all properties in a single method in the base class. So it really feels more natural to define a class variable containing an array of new properties for each subclass and manually go down the cls.__mro__ to retrieve all properties.

What I've come up with (below) seems to work relatively transparently but is it idiomatic? Is there a more typical coding pattern for this? Is there a way to avoid decorating all subclasses?

class Base(object):
    pass

class InheritClassVariable:
    def __init__(self, var, base):
        self.var = var
        self.base = base
    def __call__(self, cls):
        name = self.var
        uname = '_' + name
        bases = [B for B in cls.__mro__ if issubclass(B, self.base)]
        setattr(cls, uname, getattr(cls, name, []))
        value = [item for B in bases for item in getattr(B, uname, [])]
        setattr(cls, name, value)
        return cls

@InheritClassVariable('X', Base)
class A(Base):
    X = ['propA1', 'propA2']

@InheritClassVariable('X', Base)
class B(Base):
    X = ['propB']

@InheritClassVariable('X', Base)
class C(A):
    X = ['propC']

@InheritClassVariable('X', Base)
class D(C,B,A):
    X = ['propD']

if __name__ == "__main__":
    print(f"D.X = {D.X}")
like image 963
loqueelviento Avatar asked Oct 27 '22 19:10

loqueelviento


1 Answers

A commentator mentioned metaclasses, something I didn't know of. I looked it up and found out there is an __init_subclass__ method that is meant to avoid some of the uses of metaclasses.

That being known, I could simplify my code to (edited):

def InheritConstantArray(varname, value=[]):
    """Return a class making the 'varname' array to be inherited in subclasses"""
    basename = f"InheritConstantArray{varname}"

    def init_subclass(cls):

        # it seems __class__ won't work here.  I still don't understand
        # why.  All I know is eval() is dirty so I do a lookup.
        allbases = cls.mro()
        base = [b for b in allbases if b.__name__ == basename][0]

        # collaborate with other classes using __init_subclass__().
        # if we want sevaral variables to be inherited.
        super(base, cls).__init_subclass__()

        # base._X[cls] will store original cls.X
        uvarname = f'_{varname}' if varname[0] != '_' else f'{varname}_'
        if not hasattr(base, uvarname):
            setattr(base, uvarname,  {base: value})
        stored_values = getattr(base, uvarname)
        stored_values[cls] = cls.__dict__.get(varname, [])

        # Then we set cls.X value from base classes
        bases = [b for b in allbases if issubclass(b, base)]
        values = [i for b in bases for i in stored_values.get(b, [])]
        print(cls, base)
        setattr(cls, varname, values)

    dct = {varname: value, '__init_subclass__': init_subclass}
    base = type(basename, (object,), dct)

    return base

class A(InheritConstantArray('X')):
    X = ['A']

class B(A):
    X = ['B']

class C(A):
    X = ['C']

class D(B,C,InheritConstantArray('Y')):
    X = ['D']
    Y = ['d']

class E(D):
    X = ['E']
    Y = ['e']

class F(D):
    X = ['F']
    Y = ['f']

class G(E,F):
    X = ['G']
    Y = ['g']

if __name__ == "__main__":
    print(f"A.X = {A.X}")
    print(f"B.X = {B.X}")
    print(f"C.X = {C.X}")
    print(f"D.X = {D.X} {D.Y}")
    print(f"E.X = {E.X} {E.Y}")
    print(f"F.X = {F.X} {F.Y}")
    print(f"G.X = {G.X} {G.Y}")


I'm still unsure if it's the standard way to do it, though. (Yes, there is a very strong rationale to have class variables and multiple inheritance in my real-world problem.)

like image 177
loqueelviento Avatar answered Nov 15 '22 07:11

loqueelviento