Невозможно установить атрибут для подклассов namedtuple

Похоже, этот или этот - несколько связанных потоков, но до сих пор не понял:)

Я пытаюсь создать подкласс namedtuple и предоставить разные инициализаторы, чтобы я мог строить объекты по-разному. Например:

>>> from collections import namedtuple
>>> class C(namedtuple("C", "x, y")) :
...     __slots__ = ()
...     def __init__(self, obj) : # Initialize a C instance by copying values from obj
...         self.x = obj.a
...         self.y = obj.b
...     def __init__(self, x, y) : # Initialize a C instance from the parameters
...         self.x = x
...         self.y = y

Однако это не работает:

>>> c = C(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __init__
AttributeError: can't set attribute

После некоторого прокрутки (например, см. этот поток) Я попытался использовать конструкторы вместо инициализаторов:

>>> from collections import namedtuple
>>> class C(namedtuple("C", "x, y")) :
...     __slots__ = ()
...     def __new__(cls, obj) :
...       self = super(C, cls).__new__(cls, obj.a, obj.b)
...     def __new__(cls, x, y) :
...       self = super(C, cls).__new__(cls, x, y)

который, казалось, создал объект, но затем я не могу прочитать его атрибуты:

>>> c = C(1,2)
>>> c.x, c.y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'x'

Где я здесь ошибаюсь? Как создать подкласс с несколькими конструкторами или инициализаторами?

Ответ 1

Именованные кортежи неизменяемы, поэтому вы не можете манипулировать ими в инициализаторе __init__. Единственный вариант - переопределить метод __new__:

class C(namedtuple('C', 'x, y')):
    __slots__ = ()
    def __new__(cls, obj):
        return super(C, cls).__new__(cls, obj.x, obj.y)

Обратите внимание, что поскольку __new__ - это метод factory для новых экземпляров, вам нужно вернуть вновь созданный экземпляр. Если вы не используете return в методе __new__, возвращаемое по умолчанию значение None, что дает вам вашу ошибку.

Демо с объектом с атрибутами x и y:

>>> class C(namedtuple('C', 'x, y')):
...     __slots__ = ()
...     def __new__(cls, obj):
...         return super(C, cls).__new__(cls, obj.x, obj.y)
... 
>>> O.x, O.y
(10, 20)
>>> C(O)
C(x=10, y=20)

Python не поддерживает перегрузку метода; обычно вы либо используете необязательные аргументы ключевого слова, либо дополнительные методы класса в качестве методов factory.

datetime модуль, например, имеет несколько таких методов factory, которые позволяют создавать объекты, не соответствующие стандартным конструкторам. datetime.datetime.fromtimestamp() создает экземпляр datetime.datetime из одного числового значения, а также datetime.datetime.fromordinal(); за исключением того, что они интерпретируют число по-разному.

Если вы хотите поддерживать переменные аргументы, выполните следующие действия:

class C(namedtuple('C', 'x, y')):
    __slots__ = ()

    def __new__(cls, x, y=None):
        if y is None:
            # assume attributes
            x, y = x.x, x.y
        return super(C, cls).__new__(cls, x, y)

Здесь y - необязательный аргумент, по умолчанию None, если он не указан вызывающим:

>>> C(3, 5):
C(x=3, y=5)
>>> C(O)
C(x=10, y=20)

Альтернативой, используя метод класса, будет:

class C(namedtuple('C', 'x, y')):
    @classmethod
    def from_attributes(cls, obj):
        return cls(obj.x, obj.y)

Теперь существует два метода factory; один по умолчанию и один по имени:

>>> C(3, 5):
C(x=3, y=5)
>>> C.from_attributes(O)
C(x=10, y=20)

Ответ 2

Две вещи: одна, вы на самом деле не получаете много от именованного здесь, насколько я могу судить. Так что, возможно, вам стоит просто переключиться на обычный класс. Кроме того, вы не можете перегрузить

Во-вторых, другие возможности, которые могут помочь в вашей проблеме:

Factory шаблон дизайна - вместо того, чтобы поместить различные параметры в конструктор, введите класс, который принимает различные параметры и вызывает конструктор с соответствующими аргументами, вне объекта. recordtype - измененный namedtuple, который позволяет устанавливать значения по умолчанию, но также позволит вам написать свой подкласс так, как вы хотели. bunch - не совсем именованный кортеж, но позволяет создавать несколько произвольных объектов.

Ответ 3

Существует способ обхода атрибута namedtuple.

import collections

def updateTuple(NamedTuple,nameOfNamedTuple):
    ## Convert namedtuple to an ordered dictionary, which can be updated
    NamedTuple_asdict = NamedTuple._asdict()

    ## Make changes to the required named attributes
    NamedTuple_asdict['path']= 'www.google.com'

    ## reconstruct the namedtuple using the updated ordered dictionary
    updated_NamedTuple = collections.namedtuple(nameOfNamedTuple, NamedTuple_asdict.keys())(**NamedTuple_asdict)

    return updated_NamedTuple

Tuple = collections.namedtuple("Tuple", "path")
NamedTuple = Tuple(path='www.yahoo.com')
NamedTuple = updateTuple(NamedTuple, "Tuple")

Ответ 4

Я предлагаю вам использовать метод _replace

from collections import namedtuple
C = namedtuple('C', 'x, y')
c = C(x=10, y=20)
# c.x = 30 won't work
c = c._replace(x=30)