3

Is it possible to create a class that inherits from multiple instances of namedtuple, or create something to the same effect (having an immutable type that combines the fields of the base types)? I haven't found a way to do so.

This example illustrates the problem:

>>> class Test(namedtuple('One', 'foo'), namedtuple('Two', 'bar')):
>>>    pass

>>> t = Test(1, 2)
TypeError: __new__() takes 2 positional arguments but 3 were given

>>> t = Test(1)
>>> t.foo
1
>>> t.bar
1

The problem seems to be that namedtuple does not use super to initialize its base class, as can be seen when creating one:

>>> namedtuple('Test', ('field'), verbose=True)
[...]    
class Test(tuple):
[...]
    def __new__(_cls, field,):
        'Create new instance of Test(field,)'
        return _tuple.__new__(_cls, (field,))

Even if I considered writing my own version of namedtuple to fix this, it is not obvious how to do that. If there are multiple instances of namedtuple in the MRO of a class they'd have to share a single instance of the base class tuple. To do that, they'd have to coordinate on which namedtuple uses which range of indices in the base tuple.

Is there any simpler way to achieve multiple inheritance with a namedtuple or something similar? Has anyone already implemented that somewhere?

Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
  • How would you propose to resolve the ambiguity if the super classes contained a different number of fields or different field names? – Joel Cornett Mar 09 '15 at 11:46
  • I don't understand the question. I'd expect everything to behave the same as it would with regular mutable classes, except that it would be immutable. The sub-class would have all the fields of all the base classes. Thus, the example above would be equivalent to `namedtuple(Test, ('foo', 'bar'))`. – Björn Pollex Mar 09 '15 at 11:49
  • 1
    The issue is that since you don't define `Test.__init__`, only the `__init__` of the *first* base class is called, and that function only takes one (additional) argument. – chepner Mar 09 '15 at 14:10

3 Answers3

6

You could use a decorator or metaclass to combined the parent named tuple fields into a new named tuple and add it to the class __bases__:

from collections import namedtuple

def merge_fields(cls):
    name = cls.__name__
    bases = cls.__bases__

    fields = []
    for c in bases:
        if not hasattr(c, '_fields'):
            continue
        fields.extend(f for f in c._fields if f not in fields)

    if len(fields) == 0:
        return cls

    combined_tuple = namedtuple('%sCombinedNamedTuple' % name, fields)
    return type(name, (combined_tuple,) + bases, dict(cls.__dict__))


class SomeParent(namedtuple('Two', 'bar')):

    def some_parent_meth(self):
        return 'method from SomeParent'


class SomeOtherParent(object):

    def __init__(self, *args, **kw):
        print 'called from SomeOtherParent.__init__ with', args, kw

    def some_other_parent_meth(self):
        return 'method from SomeOtherParent'


@merge_fields
class Test(namedtuple('One', 'foo'), SomeParent, SomeOtherParent):

    def some_method(self):
        return 'do something with %s' % (self,)


print Test.__bases__
# (
#   <class '__main__.TestCombinedNamedTuple'>, <class '__main__.One'>, 
#   <class '__main__.SomeParent'>, <class '__main__.SomeOtherParent'>
# )
t = Test(1, 2)  # called from SomeOtherParent.__init__ with (1, 2) {} 
print t  # Test(foo=1, bar=2)
print t.some_method()  # do something with Test(foo=1, bar=2)
print t.some_parent_meth()  # method from SomeParent
print t.some_other_parent_meth()  # method from SomeOtherParent
Dinoboff
  • 2,622
  • 2
  • 26
  • 26
5

This code adopts a similar approach to Francis Colas', although it's somewhat longer :)

It's a factory function that takes any number of parent namedtuples, and creates a new namedtuple that has all the fields in the parents, in order, skipping any duplicate field names.

from collections import namedtuple

def combined_namedtuple(typename, *parents):
    #Gather fields, in order, from parents, skipping dupes
    fields = []
    for t in parents:
        for f in t._fields:
            if f not in fields:
                fields.append(f)
    return namedtuple(typename, fields)

nt1 = namedtuple('One', ['foo', 'qux'])
nt2 = namedtuple('Two', ['bar', 'baz'])    

Combo = combined_namedtuple('Combo', nt1, nt2)    
ct = Combo(1, 2, 3, 4)
print ct

output

Combo(foo=1, qux=2, bar=3, baz=4)
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
3

Well, if you just want a namedtuple with both the fields, it's easy to just recreate it:

One = namedtuple('One', 'foo')
Two = namedtuple('Two', 'bar')
Test = namedtuple('Test', One._fields+Two._fields)
Francis Colas
  • 3,459
  • 2
  • 26
  • 31
  • That works for the example I have given, but the real situation is slightly more complicated. In my real use case I have a class that inherits directly and indirectly from `namedtuple`. Apparently I have oversimplified my example. – Björn Pollex Mar 09 '15 at 13:03
  • Ok, can you rewrite a less minimal example? Because I don't know if composition wouldn't be a better approach in this case. – Francis Colas Mar 09 '15 at 13:31