python:int的子类的子类

问题描述 投票:1回答:1

我正在尝试了解如何正确地对int进行子类化。一个目标是在某种二进制文件格式内定义结构中使用的类型。例如,一个无符号的16位整数。我定义了一个类,如下所示,它似乎可以完成我期望的工作:

class uint16(int):

    def __new__(cls, val):
        if (val < 0 or val > 0xffff):
            raise ValueError("uint16 must be in the range %d to %d" % (0, 0xffff))
        return super(cls, cls).__new__(cls, val)

现在,我对super的使用并不清楚,因为没有参数与(类型,对象)对(类型,类型)的比较。我使用了super(cls, cls),就像在类似情况下看到的那样。

现在,使用C可以轻松创建实际上是现有类型别名的类型。例如,

typedef unsigned int        UINT;

别名可能被认为有助于澄清类型的预期用法。无论是否同意,有时都可以使用二进制格式的描述来完成此操作,如果可以,那么为清楚起见,将其复制到Python中将很有帮助。

因此,我尝试了以下操作:

class Offset16(uint16):

    def __new__(cls, val):
        return super(cls, cls).__new__(cls, val)

我本可以将Offset16设为int的子类,但随后我想重复验证(重复的代码更多)。通过子类化uint16,可以避免重复的代码。

但是当我尝试构造一个Offset16对象时,出现了递归错误:

>>> x = Offset16(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __new__
  File "<stdin>", line 5, in __new__
  File "<stdin>", line 5, in __new__
  File "<stdin>", line 5, in __new__
  [Previous line repeated 987 more times]
  File "<stdin>", line 3, in __new__
RecursionError: maximum recursion depth exceeded in comparison
>>> 

由于调用堆栈仅重复了第5行(不是交替的第3/5行,所以uint16.__new__中的行将重新输入。

然后我尝试以不同的方式修改Offset16.__new__,将args更改为super,但大多数方法都无效。但是,最后的尝试是:

class Offset16(uint16):

    def __new__(cls, val):
        return super(uint16, cls).__new__(cls, val)

这似乎起作用:

>>> x = Offset16(42)
>>> x
42
>>> type(x)
<class '__main__.Offset16'>

为什么有区别?

[后一种方法似乎破坏了super的部分目的:避免引用基类以使其易于维护。有没有一种方法可以使这项工作不需要在uint16实现中引用__new__

什么是最好的方法?

python inheritance new-operator
1个回答
0
投票

评论提供的信息有助于回答为什么有区别?最好的方法是什么?] >>>


第一:为什么有区别?

uint16Offset16的原始定义中,__new__方法使用super(cls,cls)。正如@ juanpa.arrivillaga指出的那样,当调用Offset16.__new__时,它将导致uint16.__new__递归调用自身。通过使Offset16.__new__使用super(uint16,cls),它可以更改uint16.__new__内部的行为。

一些其他解释可能有助于理解:

传递给clsOffset16.__new__参数是Offset16类本身。因此,当该方法的实现引用cls时,即引用了Offset16。因此,

    return super(cls, cls).__new__(cls, val)

在这种情况下等于

    return super(Offset16, Offset16).__new__(Offset16, val)

[现在我们可能认为super返回基类,但是提供参数时其语义更微妙:super正在解析对方法的引用,并且参数影响该解析的发生方式。如果未提供任何参数,则super().__new__是直接超类中的方法。提供参数时,会影响搜索。特别是对于super(type1, type2),将在type2的MRO(方法解析顺序)中搜索type1的出现,并使用该序列中的class following type1

documentation of super中对此进行了解释,尽管措辞可能更清楚。)

super的MRO为(Offset16,uint16,int,object)。因此

Offset16

解析为

    return super(Offset16, Offset16).__new__(Offset16, val)

当以这种方式调用 return uint16.__new__(Offset16, val) 时,传递给它的类参数是uint16.__new__,而不是Ofset16。结果,当其实现有

uint16

再次将解析为

    return super(cls, cls).__new__(cls, val)

这就是为什么我们最终陷入无限循环。

但是在 return uint16.__new__(Offset16, val) 的更改的定义中,

Offset16

最后一行等效于

class Offset16(uint16):

    def __new__(cls, val):
        return super(uint16, cls).__new__(cls, val)

并且根据 return super(uint16, Offset16).__new__(Offset16, val) 的MRO和上述Offset16的语义,解析为

super

这说明了为什么更改的定义会导致不同的行为。


第二:最佳方法是什么?

注释中提供了不同的替代方案,可能适合不同的情况。

return int.__new__(Offset16, val) 建议(假设使用Python3),只需使用不带参数的@juanpa.arrivillaga。对于问题中采用的方法,这是有道理的。将参数传递给super()的原因是要操纵MRO搜索。在这个简单的类层次结构中,不需要。

super建议直接引用特定的超类,而不要使用@Jason Yang。例如:

super

对于这种简单情况,这是完全可以的。但是对于其他具有更复杂类关系的方案而言,这可能不是最好的方法。注意,例如,上面重复了class Offset16(uint16): def __new__(cls, val): return uint16.__new__(cls, val) 。如果子类有多个包装(而不是替换)超类方法的方法,则将有很多重复的引用,并且对类层次结构进行更改将导致难以分析的错误。避免此类问题是使用uint16的预期好处之一。

最后,super建议仅使用

@Adam.Er8

的确非常简单。一个警告是,Offset16 = uint16 确实不过是Offset16的别名。它不是一个单独的类。例如:

uint16

所以,这可能很好,只要应用程序中不需要真正的类型区别

© www.soinside.com 2019 - 2024. All rights reserved.