在Python中,当一个类是一次性的namedtuple时,我该如何调用其父类?

15

所以,我有一个用于串行API的大量消息负载类,每个类都有一些不可变字段、一个解析方法和一些共享的方法。我的结构方式是,每个类都将继承命名元组以获得字段行为,并从父类接收共同的方法。然而,我在构造函数方面遇到了一些困难:

class Payload:
    def test(self):
        print("bar")

class DifferentialSpeed(Payload, namedtuple('DifferentialSpeed_', 
    'left_speed right_speed left_accel right_accel')):
    __slots__ = ()
    def __init__(self, **kwargs):
        super(DifferentialSpeed, self).__init__(**kwargs)
        # TODO: Field verification
        print("foo")

    @classmethod
    def parse(self, raw):
        # Dummy for now
        return self(left_speed = 0.0, right_speed = 0.1,
                    left_accel = 0.2, right_accel = 0.3)

    def __str__(self):
        return "Left Speed: %fm/s\nRight Speed: %fm/s\n"\
            "Left Acceleration: %fm/s^2\nRight Acceleration: %fm/s^2" % (
            self.left_speed, self.right_speed, self.left_accel, self.right_accel)


payload = DifferentialSpeed.parse('dummy')
print(payload)

这个方法可以工作,但我收到了以下警告:

DeprecationWarning: object.__init__() takes no parameters
  super(DifferentialSpeed, self).__init__(**kwargs)
如果我从调用中删除**kwargs,似乎它仍然可以工作,但是为什么?这些构造函数的参数是如何传递给namedtuple的?这是保证的吗,还是mro建立的随机结果?
如果我想避免使用super,按照旧的方式做,有没有办法访问namedtuple以调用其构造函数?我不想这样做:
DifferentialSpeed_ = namedtuple('DifferentialSpeed_', 
    'left_speed right_speed left_accel right_accel')
class DifferentialSpeed(Payload, DifferentialSpeed_):

看起来有些啰嗦和不必要。

在这种情况下,我的最佳行动方案是什么?


1
请注意,如果您尝试使用namedtuple来节省内存,则需要在派生类以及其他继承的Payload类中设置__slots__ =(),否则该类仍将具有__dict__ - Glenn Maynard
3个回答

28

首先,namedtuple(whatever) 继承自不可变的 tuple 类型,而不可变类型不需要关注 __init__,因为在调用 __init__ 时对象已经被构造好了。如果你想传递参数到 namedtuple 基类,你需要重载 __new__ 方法。

通过传入 verbose=true 参数来查看 namedtuple() 结果的定义;我发现这很有教育意义。


1
哦,好的。非常有趣。对于将事物传递给namedtuple,我并不在意;我只是想确保它不会错过其参数。也许我可以完全忽略整个super()问题。 - mikepurvis
好的,我会接受这个答案,因为它既有帮助性又简洁明了。我想最终我可能会选择在Payload中使用一个通用构造函数,该函数调用多态的"self.verify()",然后根据需要抛出异常。 - mikepurvis

5
你有三个基类: Payload,你的命名元组DifferentialSpeed_和通用基类object。前两者都没有__init__函数,除了从object继承的一个之外。由于不可变类的初始化是由__new__完成的,因此namedtuple不需要__init__,在运行__init__之前调用__new__即可。
由于super(DifferentialSpeed, self).__init__解析为调用链中的下一个__init__,所以下一个__init__object.__init__,这意味着您正在向该函数传递参数。它不需要任何参数,没有理由将参数传递给object.__init__
(以前它接受并静默忽略参数。这种行为正在消失,它在Python 3中已经消失,这就是为什么会得到DeprecationWarning的原因。)
通过添加不带参数的Payload.__init__函数,可以更清楚地触发问题。当您尝试传递`*kwargs`时,它会引发错误。
在这种情况下正确的做法几乎肯定是删除**kwargs参数,并只调用super(DifferentialSpeed, self).__init__()。它不需要任何参数;DifferentialSpeed正在将其自己的参数传递给Payload,并且调用链中更深层的函数对此一无所知。

你的陈述“在这种情况下正确的做法几乎肯定是删除**kwargs参数,然后只调用super(DifferentialSpeed, self).__init__(**kwargs)”似乎有矛盾之处,最后一部分不应该是“只调用super(DifferentialSpeed, self).__init__()”吗? - martineau
1
如果有其他人到达这里,请注意还有另一个原因会导致此警告出现或消失;请参阅Objects/typeobject.c中关于__new__的注释。(我不想详细解释,因为提问者完全忽略了这个答案,而接受了一个甚至没有回答他问题的答案...) - Glenn Maynard
感谢您的纠正和澄清。也许OP忽略了您的答案,因为它并不完全合理。无论如何,关于__new __()何时产生有关参数的警告的规则确实很复杂。对于任何感兴趣的人,您可以在此处查看来自typecobject.c的相关注释副本(http://dl.dropbox.com/u/5508445/stackoverflow/typeobject.c_excerpt.txt),或在svn中查看整个源文件(http://svn.python.org/view/python/trunk/Objects/typeobject.c?revision=81744&view=markup)。 - martineau

3
正如其他人所指出的,元组是一种不可变类型,必须在它们的__new__()而不是__init__()方法中进行初始化--因此您需要在子类中添加前者(并且删除后者)。以下是如何将此应用于您的示例代码。唯一的其他更改是在开头添加了一个from import...语句。
注意:在__new__()中的super()调用中必须两次传递cls,因为它是一个静态方法,尽管它被特殊处理,因此您不必声明它为静态方法。
from collections import namedtuple

class Payload:
    def test(self):
        print("bar")

class DifferentialSpeed(Payload, namedtuple('DifferentialSpeed_',
    'left_speed right_speed left_accel right_accel')):
    #### NOTE: __new__ instead of an __init__ method ####
    def __new__(cls, **kwargs):
        self = super(DifferentialSpeed, cls).__new__(cls, **kwargs)
        # TODO: Field verification
        print("foo")
        return self

    @classmethod
    def parse(self, raw):
        # Dummy for now
        return self(left_speed = 0.0, right_speed = 0.1,
                    left_accel = 0.2, right_accel = 0.3)

    def __str__(self):
        return "Left Speed: %fm/s\nRight Speed: %fm/s\n"\
            "Left Acceleration: %fm/s^2\nRight Acceleration: %fm/s^2" % (
            self.left_speed, self.right_speed, self.left_accel, self.right_accel)


payload = DifferentialSpeed.parse('dummy')
print(payload)

不需要碰 __new__。只有在想要修改 DifferentialSpeed_ 初始化参数时才需要这样做;他只是在验证它们。 - Glenn Maynard
@Glenn Maynard: 我觉得__new __()需要参与进来,这样参数就可以在分配给元组之前进行验证(因为它们以后无法更改)。例如,它们可以被赋予默认值,或者在没有任何伪造的赋值的情况下引发异常。 - martineau
1
__init__引发异常同样有效。(悄悄地强制执行默认值听起来不像是明智的行为。) - Glenn Maynard
是的,我的意图只是从__init__中引发异常。这实际上是一个双向消息有效载荷,因此它可以从解析数据实例化,也可以直接从构造函数实例化。验证主要是为了非解析行为的好处。 - mikepurvis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接