python:int子类的子类

2024-06-02 11:08:50 发布

您现在位置:Python中文网/ 问答频道 /正文

我试图理解如何正确地将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的部分目的:避免对基类的引用以使其更易于维护。有没有一种方法可以使这项工作不需要在__new__实现中引用uint16

最好的方法是什么


Tags: 方法in类型newdefstdinlineval
1条回答
网友
1楼 · 发布于 2024-06-02 11:08:50

评论提供的信息有助于回答为什么会有差异和最好的方法是什么


第一:为什么不同

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

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

传递到Offset16.__new__cls参数是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的出现,并且将使用该序列中type1后面的

(这在documentation of ^{}中有解释,不过措辞可能更清楚。)

Offset16的MRO是(Offset16,uint16,int,object)。所以

    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)

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

        return int.__new__(Offset16, val)

这就解释了为什么改变定义会导致不同的行为


第二:最好的方法是什么

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

@juanpa.arrivillaga建议(假设Python3)只使用super()而不使用参数。对于问题中采取的方法来说,这是有意义的。将参数传递给super的原因是为了操纵MRO搜索。在这个简单的类层次结构中,这是不需要的

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

class Offset16(uint16):

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

对于这种简单的情况,这是非常好的。但对于具有更复杂类关系的其他场景,它可能不是最好的。例如,请注意uint16在上面的示例中是重复的。如果子类有几个方法包装(而不是替换)了超类方法,那么会有许多重复引用,对类层次结构进行更改将导致难以分析的错误。避免这些问题是使用super的预期好处之一

最后,@Adam.Er8建议只使用

Offset16 = uint16

事实上,这很简单。一个警告是Offset16实际上只不过是uint16的别名;这不是一门单独的课。例如:

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

因此,只要应用程序中不需要实际的类型区分,这就可以了

相关问题 更多 >