无法为 namedtuple 的子类设置属性

Posted

技术标签:

【中文标题】无法为 namedtuple 的子类设置属性【英文标题】:Can't set attribute for subclasses of namedtuple 【发布时间】:2013-10-15 22:54:03 【问题描述】:

看起来this 或this 是一些相关的线程,但还没有弄清楚:)

我正在尝试创建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

经过一番探索(例如,参见this 线程),我尝试使用构造函数而不是初始化器:

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

我哪里错了?如何创建具有多个构造函数或初始化程序的子类?

【问题讨论】:

为什么你有双重__init____new__方法?只有第二个重要,它会覆盖第一个。 Python 不会“重载”方法签名。 无重载...所以这意味着我以不同方式(取决于重载的构造函数)创建 C 实例的最初目标实际上是不可行的? 完全可行,只是使用不同的范例。 在您的回答下方查看我的评论。你说使用工厂方法可行,但不能使用多个“构造函数”? 你可以只使用一个构造函数,但是构造函数可以根据你传入的参数改变行为。 【参考方案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__ 是新实例的工厂方法,您确实需要返回新创建的实例。如果您在__new__ 方法中不使用return,则默认返回值是None,这会给您带来错误。

带有xy 属性的对象的演示:

>>> 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 不支持方法重载;通常你要么使用可选的关键字参数,要么使用额外的类方法作为工厂方法。

例如,datetime module 有几个这样的工厂方法可以让您创建不适合标准构造函数的对象。 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)

现在有两种工厂方法;一个默认,一个命名:

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

【讨论】:

谢谢马丁。在不重载的情况下,我决定使用构造函数(接收xy)和第二个工厂方法(接收obj)。不漂亮,也许我更喜欢 C++ 风格的构造函数重载,但我想这与 Python 一样多。 使用_replace方法怎么样? docs.python.org/2/library/… @YonatanSimson:使用它怎么样?你能详细说明吗?请注意,instance._replace() 返回一个新实例,原始实例保持不变。 ._replace() 不会让工厂函数更高效。 @MartijnPieters 是的。你可以这样做:c = c._replace(x=30)。我觉得即使有一些开销,也最好使用现有的功能。如果没有相同级别的测试和置信度,编写自己的方法可能会产生相同的间接成本 @YonatanSimson:你会如何在这里使用它?用户要求使用工厂方法来创建新对象。 _replace()现有实例上效果很好,但这不是这里所要求的。【参考方案2】:

两件事:第一,据我所知,您在这里并没有真正从 namedtuple 中得到什么。所以也许你应该换成普通班。另外,你不能超载

其次,可能有助于解决您的问题的其他可能性:

Factory design pattern - 不是将不同的参数放在构造函数中,而是有一个类,它接受不同类型的参数并在对象外部使用适当的参数调用构造函数。 recordtype - 一个可变的命名元组,它允许使用默认值,但也可以让您以最初想要的方式编写子类。 bunch - 不完全是命名元组,但可以让您创建一些随意的对象。

【讨论】:

【参考方案3】:

有一种解决方法可以更改命名元组的属性。

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)

【讨论】:

以上是关于无法为 namedtuple 的子类设置属性的主要内容,如果未能解决你的问题,请参考以下文章

子类化集合命名元组

一种将 NamedTuple 子类化以进行类型检查的方法

访问 URLSessionConfiguration 子类的属性时无法识别的选择器

Python:无法腌制类型 X,属性查找失败

`namedtuple` 在内存使用上真的和元组一样高效吗?我的测试说不

如何使用 swift 在 UIResponder 子类中将属性重新声明为读写?