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.
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')