-->

How to use namedtuples in multiple inheritance

2019-01-27 04:07发布

问题:

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?

回答1:

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


回答2:

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)


回答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)