Search code examples
pythonpython-3.xpython-2.7multiple-inheritancenamedtuple

Accessing derived attributes in a named tuple from parent class


I have a simple named tuple which functions as a immutable container that characterize a class I designed.

From this Class (that has the namedtuple as class variable), I dervied multiple childclasses that all override this attribute and add new fields to the named tuple

I want to keep the fields that were defined in the Parent class ID and only add the ones that are new. I assume you could simply keep the old ID in the class and do something like ID.2nd = "added form child" But I'd much prefere if you could simply override the ID variable and acess the previously defined ID's via a call to super() or something

from collections import namedtuple

ID = namedtuple("myTuple", ["first", "second", "third", "fourth"])
ID.__new__.__defaults__ = ("default from parentclass", None , None , None)


class Parent(object):
    _ID = ID()

    def __init__(self, arg):
        self.arg = arg

class Child(Parent):
    #if there is a new _ID defined in Child
    #, keep the fields it adds but also add the
    #ones from the parent:
    _ID = ID(second="adding from child")
    #it should now contain the fields _ID.first == "default from parent" from parentclass
    #and _ID.second == "adding from child"

So far this works, but if I have another child now

class Child2(Child):

    _ID = ID(third="adding from child")
    #and now something like _ID.second = super()._ID.second

I will lose all information that was added in the intermediate classes.


Solution

  • That can only work incorporating the same logic that class inheritance has into your NamedTuple attributes - there is no way it could work with stand alone NamedTuple resources: the mechanism have to know, somehow, about your inheritance chain.

    A nice place to do that, is to hook something when each subclass is created - for Python versions prior to 3.6, that would imply creating a metaclass. But since, the special class method __init_subclass__ can do the job.

    What has to be done is somewhat straightforward. However, the usual mechanism for fetching things from super-classes, super() won't work for class attribute, due to the way it is designed - so we have to reimplement it - I choose to do that in a one liner that will just skip superclasses until one that explicitly defines the _ID attribute is found (the code would be barely readable without this context) - and then we have some extra code to extract the non-default arguments and merge them in a new ID instance.

    This is the code:

    from collections import namedtuple
    
    ID = namedtuple("myTuple", ["first", "second", "third", "fourth"])
    ID.__new__.__defaults__ = ("default from parentclass", None , None , None)
    
    
    class A:
        _ID = ID()
        def __init_subclass__(cls, *args, **kw):
            super().__init_subclass__(*args, **kw)
            if "_ID" in cls.__dict__:
                _sentinel = object()
                new_fields = [cls_v if cls_v != default_v else _sentinel for cls_v, default_v in zip(cls._ID, ID())]
                super_id = next(itertools.dropwhile(lambda scls: "_ID" not in scls.__dict__, cls.__mro__[1:]))._ID
                final_fields = [cls_v if cls_v is not _sentinel else super_v for cls_v, super_v in zip(new_fields, super_id)]
                cls._ID = ID(*final_fields)
    
    
    

    And after pasting this in a REPL we have:

    In [31]: class B(A):
        ...:     _ID = ID(second="blip")
        ...: 
    
    In [32]: B._ID
    Out[32]: myTuple(first='default from parentclass', second='blip', third=None, fourth=None)
    
    In [33]: class C(B):
        ...:     _ID = ID(fourth="blop")
        ...: 
    
    In [34]: C._ID
    Out[34]: myTuple(first='default from parentclass', second='blip', third=None, fourth='blop')