python: int 子类的子类

python: subclass of subclass of int

我正在尝试了解如何正确地 subclass int。一个目标是在某种二进制文件格式中定义结构中使用的类型。例如,一个无符号的 16 位整数。我定义了一个 class 如下,这似乎符合我的预期:

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 的使用(不带参数)与 (type, object) 与 (type, type)。我使用了 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 的子 class,但我想重复验证(更多重复代码)。通过 sub-classing 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__,将参数更改为 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 的部分目的:避免引用基础 class 以使其更易于维护。有没有一种方法可以使这项工作不需要在 __new__ 实现中引用 uint16

最好的方法是什么?

评论提供的信息有助于回答 为什么不同?什么是最好的方法?


第一:为什么不一样?

uint16Offset16的原始定义中,__new__方法使用super(cls,cls)。正如@juanpa.arrivillaga 所指出的,当 Offset16.__new__ 被调用时,它会导致 uint16.__new__ 递归调用自身。通过让 Offset16.__new__ 使用 super(uint16,cls),它改变了 uint16.__new__.

内部的行为

一些额外的解释可能有助于理解:

传递给 Offset16.__new__cls 参数是 Offset16 class 本身。所以,当方法的实现引用了cls,也就是引用了Offset16。所以,

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

在这种情况下等同于

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

现在我们可能认为 super 返回基数 class,但它在提供参数时的语义更加微妙:super 正在解析对方法的引用和争论会影响该决议的发生方式。如果没有提供参数,super().__new__ 是直接 superclass 中的方法。提供参数时,会影响搜索。特别是对于 super(type1, type2),将搜索 type2 的 MRO(方法解析顺序)以查找 type1 的出现,以及 type1[ 之后的 class =119=] 将使用该序列。

(这在 documentation of super 中有解释,尽管措辞可能更清楚。)

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

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

解析为

    return uint16.__new__(Offset16, val)

当以这种方式调用uint16.__new__时,传递给它的class参数是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)

并且根据 Offset16 的 MRO 和上面提到的 super 的语义,解析为

        return int.__new__(Offset16, val)

这解释了为什么更改后的定义会导致不同的行为。


第二:最好的方法是什么?

评论中提供了可能适合不同情况的不同备选方案。

@juanpa.arrivillaga 建议(假设 Python3)简单地使用 super() 而不带参数。对于问题中采用的方法,这是有道理的。将参数传递给 super 的原因是为了操纵 MRO 搜索。在这个简单的 class 层次结构中,不需要这样做。

@Jason Yang 建议直接引用特定的 superclass 而不是使用 super。例如:

class Offset16(uint16):

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

对于这种简单的情况,这完全没问题。但对于具有更复杂 class 关系的其他场景,它可能不是最好的。请注意,例如,uint16 在上面重复了。如果 subclass 有几个包装(而不是替换)superclass 方法的方法,就会有很多重复引用,并且对 class 层次结构进行更改会导致硬-分析错误。避免此类问题是使用 super 的预期好处之一。

最后,@Adam.Er8建议简单地使用

Offset16 = uint16

这确实很简单。需要注意的是 Offset16 实际上只是 uint16 的一个别名;它不是单独的 class。例如:

>>> Offset16 = uint16
>>> x = Offset16(24)
>>> type(x)
<class 'uint16'>

所以,这可能没问题只要应用程序永远不需要有实际的类型区分