правильная перегрузка __add__ namedtuple

Я пытаюсь перегрузить метод __add__ для экземпляров namedtuple, и у меня возникают проблемы.

Параметры, вводимые в мои namedtuples, генерируются динамически. Четыре параметра всегда одни и те же и в том же порядке, а остальные могут быть любыми и в любом количестве. Поэтому мне нужно иметь возможность динамически определять мою фабрику классов namedtuple. И после того, как я создам несколько экземпляров, я хотел бы иметь возможность добавить их вместе в новый экземпляр namedtuple со всеми уникальными параметрами вместе. Но у меня возникли проблемы с правильной перегрузкой метода __add__. Кажется, это не работает.

Так, например, если у меня есть 3 экземпляра namedtuple

e = Row(a=1, b=2, c=3, d=4)
m = Row(a=1, b=2, c=3, d=4, param1='a', param2='b')
t = Row(a=1, b=2, c=3, d=4, param3='val', param4=10)

Я хотел бы иметь возможность добавлять их, например e + m + t, который возвращает

Row(a=1, b=2, c=3, d=4, param1='a', param2='b', param3='val', param4=10)

Вот мой текущий код

class Row(object):
    ''' Creates a new namedtuple object '''
    __slots__ = ()

    def __new__(cls, *args, **kwargs):
        ''' make a new Row instance '''
        default = namedtuple('Row', 'a, b, c, d')
        newcols = set(args) - set(default._fields)
        finalfields = default._fields + tuple(newcols) if newcols else default._fields
        return namedtuple('Row', finalfields)

    def __add__(self, other):
        ''' This is the new add '''
        self_dict = self._asdict()
        other_dict = other._asdict()
        self_dict.update(other_dict)
        new_fields = tuple(self_dict.keys())
        new_row = namedtuple('Row', new_fields)
        return new_row(**self_dict)

Благодаря этому я могу правильно динамически генерировать новые namedtuples и создавать их экземпляры.

e = Row()
m = Row(*['a', 'b', 'c', 'd', 'param1', 'param2'])

e._fields
('a', 'b', 'c', 'd')
m._fields
('a', 'b', 'c', 'd', 'param1', 'param2')

e2 = e(1, 2, 3, 4)
m2 = m(1, 2, 3, 4, 'a', 'b')

e2
Row(a=1, b=2, c=3, d=4)
type(e2)
__main__.Row

m2
Row(a=1, b=2, c=3, d=4, param1='a', param2='b')

но когда я добавляю их, мой перегруженный __add__ никогда не вызывается, и я, кажется, просто возвращаю обычный объект кортежа

w = e2 + m2
print(w)
(1, 2, 3, 4, 1, 2, 3, 4, 'a', 'b')
type(w)
tuple

Мой метод __add__, похоже, не активен для моих объектов экземпляра.

Row.__add__?
Signature: Row.__add__(self, other)
Docstring: This is the new add
File:      <ipython-input-535-817d9f528ae7>
Type:      instancemethod

e.__add__?
Type:        wrapper_descriptor
String form: <slot wrapper '__add__' of 'tuple' objects>
Docstring:   x.__add__(y) <==> x+y

e2.__add__?
Type:        method-wrapper
String form: <method-wrapper '__add__' of Row object at 0x122614050>
Docstring:   x.__add__(y) <==> x+y

Что я делаю не так? Я также попытался создать подкласс namedtuple('Row', ...), как указано в документах https://docs.python.org/2/library/collections.html#collections.namedtuple, но я не смог заставить это работать. Я не мог заставить его динамически изменять именованные параметры.

Вот этот провал

BaseRow = namedtuple('BaseRow', 'a, b, c, d')

class Row(BaseRow):
    __slots__ = ()

    def __new__(cls, *args, **kwargs):
        new_fields = set(kwargs.keys()) - set(cls._fields)
        cls._fields += tuple(new_fields)
        obj = super(Row, cls).__new__(cls, *args, **kwargs)
        return obj

e = Row(a=1, b=2, c=3, d=4, param1='a')
TypeError: __new__() got an unexpected keyword argument 'param1'

person havok2063    schedule 18.08.2017    source источник
comment
Это... противоречит многим дизайнерским решениям namedtuple и вряд ли окажется удачным. Я рекомендую просто использовать dicts.   -  person user2357112 supports Monica    schedule 18.08.2017


Ответы (2)


Определенный вами метод __add__ — это метод, доступный только для экземпляров класса типа Row.

Когда вы переопределяете метод __new__ вашего класса Row, вы возвращаете объект типа namedtuple(...), а не Row. Следовательно, дальнейшие манипуляции с этими объектами не будут иметь доступа к вашему методу __add__, потому что они не Row, а namedtuple().

Как упомянул @user2357112, кажется, что вы усложняете себе жизнь и, возможно, лучше просто использовать словари. Если вам нужен неизменяемый, хешируемый тип для каждой из ваших строк, чтобы вы могли создавать наборы и использовать их в качестве ключей словаря, преобразуйте свои словари в именованные кортежи непосредственно перед их использованием таким образом.

person drootang    schedule 18.08.2017

Спасибо за ответы. Я как бы вынужден использовать namedtuples, потому что я имею дело с результатами, возвращаемыми SQLAlchemy, который возвращает вещи как KeyedTuples, что является их версией namedtuple. Поэтому я должен использовать namedtuple, чтобы мои общие функции могли работать с обоими. Я уверен, что это ломает весь дух кортежей.

Для потомков, вот как я это решил. Поскольку namedtuple на самом деле является просто функцией, которая генерирует классы, я просто написал свою собственную функцию, которая таким же образом будет динамически генерировать новый объект namedtuple, и перегрузил метод __add__ для каждого сгенерированного класса.

def mytuple(name, params=None, **kwargs):

    # check the params input
    if params and isinstance(params, six.string_types):
        params = params.split(',') if ',' in params else [params]
        params = [p.strip() for p in params]

    # create default namedtuple and find new columns
    default = namedtuple(name, 'a, b, c, d')
    newcols = [col for col in params if col not in default._fields] if params else None
    finalfields = default._fields + tuple(newcols) if newcols else default._fields
    nt = namedtuple(name, finalfields, **kwargs)

    def new_add(self, other):
        ''' Overloaded add to combine tuples without duplicates '''    
        self_dict = self._asdict()
        other_dict = other._asdict()
        self_dict.update(other_dict)

        new_fields = tuple(self_dict.keys())
        new_row = mytuple(self.__class__.__name__, new_fields)
        return new_row(**self_dict)

    # append new properties and overloaded methods
    nt.__add__ = new_add
    return nt

Он используется так

# create first version
nt = mytuple('Row', 'a, b, c, d')
e = nt(1,2,3,4)
e
Row(a=1, b=2, c=3, d=4)

# create second version
nt = mytuple('Row', 'a, b, c, d, param1, param2')
m = nt(1,2,3,4,'a','b')
m
Row(a=1, b=2, c=3, d=4, param1='a', param2='b')

# create third version
nt = mytuple('Row', 'a, b, c, d, param3, param4')
s = nt(1,2,3,4,'stuff',10.2345)
s
Row(a=1, b=2, c=3, d=4, param3='stuff', param4=10.2345)

# add them together
d = e + m + s
d
Row(a=1, b=2, c=3, d=4, param1='a', param2='b', param3='stuff', param4=10.2345)
person havok2063    schedule 20.08.2017